diff --git a/libs/common/src/constants/role-permissions.ts b/libs/common/src/constants/role-permissions.ts index c457c3c..8d3d0ed 100644 --- a/libs/common/src/constants/role-permissions.ts +++ b/libs/common/src/constants/role-permissions.ts @@ -46,6 +46,7 @@ export const RolePermissions = { 'VISITOR_PASSWORD_VIEW', 'VISITOR_PASSWORD_ADD', 'USER_ADD', + 'SPACE_MEMBER_ADD', ], [RoleType.ADMIN]: [ 'DEVICE_SINGLE_CONTROL', @@ -92,6 +93,7 @@ export const RolePermissions = { 'VISITOR_PASSWORD_VIEW', 'VISITOR_PASSWORD_ADD', 'USER_ADD', + 'SPACE_MEMBER_ADD', ], [RoleType.SPACE_MEMBER]: [ 'DEVICE_SINGLE_CONTROL', diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts index 472772b..cd83fa0 100644 --- a/libs/common/src/database/database.module.ts +++ b/libs/common/src/database/database.module.ts @@ -36,6 +36,7 @@ import { InviteUserEntity, InviteUserSpaceEntity, } from '../modules/Invite-user/entities'; +import { InviteSpaceEntity } from '../modules/space/entities/invite-space.entity'; @Module({ imports: [ TypeOrmModule.forRootAsync({ @@ -80,6 +81,7 @@ import { TagModel, InviteUserEntity, InviteUserSpaceEntity, + InviteSpaceEntity, ], namingStrategy: new SnakeNamingStrategy(), synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))), 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 22bbbdb..345c1b8 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 @@ -18,7 +18,7 @@ import { InviteUserDto, InviteUserSpaceDto } from '../dtos'; import { ProjectEntity } from '../../project/entities'; @Entity({ name: 'invite-user' }) -@Unique(['email', 'invitationCode']) +@Unique(['email', 'invitationCode', 'project']) export class InviteUserEntity extends AbstractEntity { @Column({ type: 'uuid', @@ -29,7 +29,6 @@ export class InviteUserEntity extends AbstractEntity { @Column({ nullable: false, - unique: true, }) email: string; diff --git a/libs/common/src/modules/space/entities/invite-space.entity.ts b/libs/common/src/modules/space/entities/invite-space.entity.ts new file mode 100644 index 0000000..72737ca --- /dev/null +++ b/libs/common/src/modules/space/entities/invite-space.entity.ts @@ -0,0 +1,35 @@ +import { Column, Entity, ManyToOne, Unique } from 'typeorm'; +import { UserSpaceDto } from '../../user/dtos'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { SpaceEntity } from './space.entity'; + +@Entity({ name: 'invite-space' }) +@Unique(['invitationCode']) +export class InviteSpaceEntity extends AbstractEntity { + @Column({ + type: 'uuid', + default: () => 'gen_random_uuid()', // Use gen_random_uuid() for default value + nullable: false, + }) + public uuid: string; + + @ManyToOne(() => SpaceEntity, (space) => space.userSpaces, { + nullable: false, + }) + space: SpaceEntity; + + @Column({ + nullable: true, + }) + public invitationCode: string; + + @Column({ + nullable: false, + default: true, + }) + public isActive: boolean; + constructor(partial: Partial) { + super(); + Object.assign(this, partial); + } +} diff --git a/libs/common/src/modules/space/entities/space.entity.ts b/libs/common/src/modules/space/entities/space.entity.ts index 8ecbc70..a0b908d 100644 --- a/libs/common/src/modules/space/entities/space.entity.ts +++ b/libs/common/src/modules/space/entities/space.entity.ts @@ -1,11 +1,4 @@ -import { - Column, - Entity, - JoinColumn, - ManyToOne, - OneToMany, - Unique, -} from 'typeorm'; +import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from 'typeorm'; import { SpaceDto } from '../dtos'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { UserSpaceEntity } from '../../user/entities'; @@ -19,7 +12,6 @@ import { InviteUserSpaceEntity } from '../../Invite-user/entities'; import { TagEntity } from './tag.entity'; @Entity({ name: 'space' }) -@Unique(['invitationCode']) export class SpaceEntity extends AbstractEntity { @Column({ type: 'uuid', @@ -44,10 +36,6 @@ export class SpaceEntity extends AbstractEntity { @JoinColumn({ name: 'community_id' }) community: CommunityEntity; - @Column({ - nullable: true, - }) - public invitationCode: string; @ManyToOne(() => SpaceEntity, (space) => space.children, { nullable: true }) parent: SpaceEntity; diff --git a/libs/common/src/modules/space/repositories/space.repository.ts b/libs/common/src/modules/space/repositories/space.repository.ts index b2aacc0..9bc2fee 100644 --- a/libs/common/src/modules/space/repositories/space.repository.ts +++ b/libs/common/src/modules/space/repositories/space.repository.ts @@ -1,6 +1,7 @@ import { DataSource, Repository } from 'typeorm'; import { Injectable } from '@nestjs/common'; import { SpaceEntity, SpaceLinkEntity, TagEntity } from '../entities'; +import { InviteSpaceEntity } from '../entities/invite-space.entity'; @Injectable() export class SpaceRepository extends Repository { @@ -22,3 +23,10 @@ export class TagRepository extends Repository { super(TagEntity, dataSource.createEntityManager()); } } + +@Injectable() +export class InviteSpaceRepository extends Repository { + constructor(private dataSource: DataSource) { + super(InviteSpaceEntity, dataSource.createEntityManager()); + } +} diff --git a/libs/common/src/modules/space/space.repository.module.ts b/libs/common/src/modules/space/space.repository.module.ts index 030c684..a475a9a 100644 --- a/libs/common/src/modules/space/space.repository.module.ts +++ b/libs/common/src/modules/space/space.repository.module.ts @@ -1,11 +1,19 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { SpaceEntity, SubspaceEntity, TagEntity } from './entities'; +import { InviteSpaceEntity } from './entities/invite-space.entity'; @Module({ providers: [], exports: [], controllers: [], - imports: [TypeOrmModule.forFeature([SpaceEntity, SubspaceEntity, TagEntity])], + imports: [ + TypeOrmModule.forFeature([ + SpaceEntity, + SubspaceEntity, + TagEntity, + InviteSpaceEntity, + ]), + ], }) export class SpaceRepositoryModule {} diff --git a/src/guards/permissions.guard.ts b/src/guards/permissions.guard.ts index ac5881e..d50ff24 100644 --- a/src/guards/permissions.guard.ts +++ b/src/guards/permissions.guard.ts @@ -1,4 +1,8 @@ -import { Injectable, ExecutionContext } from '@nestjs/common'; +import { + Injectable, + ExecutionContext, + UnauthorizedException, +} from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { Reflector } from '@nestjs/core'; import { RolePermissions } from '@app/common/constants/role-permissions'; @@ -30,14 +34,34 @@ export class PermissionsGuard extends AuthGuard('jwt') { const request = context.switchToHttp().getRequest(); const user = request.user; // User is now available after AuthGuard - const userRole = user?.role.type as RoleType; + const userRole = user?.role?.type as RoleType; if (!userRole || !RolePermissions[userRole]) { - return false; // Deny if role or permissions are missing + throw new UnauthorizedException({ + message: `Only ${this.getAllowedRoles(requiredPermissions)} role(s) can access this route.`, + }); } const userPermissions = RolePermissions[userRole]; + const hasRequiredPermissions = requiredPermissions.every((perm) => + userPermissions.includes(perm), + ); - // Check if the user has the required permissions - return requiredPermissions.every((perm) => userPermissions.includes(perm)); + if (!hasRequiredPermissions) { + throw new UnauthorizedException({ + message: `Only ${this.getAllowedRoles(requiredPermissions)} role(s) can access this route.`, + }); + } + + return true; + } + + private getAllowedRoles(requiredPermissions: string[]): string { + const allowedRoles = Object.entries(RolePermissions) + .filter(([, permissions]) => + requiredPermissions.every((perm) => permissions.includes(perm)), + ) + .map(([role]) => role); + + return allowedRoles.join(', '); } } diff --git a/src/invite-user/controllers/invite-user.controller.ts b/src/invite-user/controllers/invite-user.controller.ts index 8f65993..1a5ac35 100644 --- a/src/invite-user/controllers/invite-user.controller.ts +++ b/src/invite-user/controllers/invite-user.controller.ts @@ -76,9 +76,7 @@ export class InviteUserController { async activationCodeController( @Body() activateCodeDto: ActivateCodeDto, ): Promise { - return await this.inviteUserService.activationCodeController( - activateCodeDto, - ); + return await this.inviteUserService.activationCode(activateCodeDto); } @ApiBearerAuth() @UseGuards(JwtAuthGuard) diff --git a/src/invite-user/invite-user.module.ts b/src/invite-user/invite-user.module.ts index fc0b579..6b77d1d 100644 --- a/src/invite-user/invite-user.module.ts +++ b/src/invite-user/invite-user.module.ts @@ -15,17 +15,22 @@ import { 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 { + InviteSpaceRepository, + 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 { UserService, 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'; import { RoleTypeRepository } from '@app/common/modules/role-type/repositories'; +import { RegionRepository } from '@app/common/modules/region/repositories'; +import { TimeZoneRepository } from '@app/common/modules/timezone/repositories'; @Module({ imports: [ConfigModule, InviteUserRepositoryModule], @@ -51,6 +56,10 @@ import { RoleTypeRepository } from '@app/common/modules/role-type/repositories'; PermissionTypeRepository, ProjectUserService, RoleTypeRepository, + InviteSpaceRepository, + UserService, + RegionRepository, + TimeZoneRepository, ], exports: [InviteUserService], }) diff --git a/src/invite-user/services/invite-user.service.ts b/src/invite-user/services/invite-user.service.ts index 5b7807b..8d63959 100644 --- a/src/invite-user/services/invite-user.service.ts +++ b/src/invite-user/services/invite-user.service.ts @@ -29,6 +29,7 @@ import { UpdateUserInvitationDto, } from '../dtos/update.invite-user.dto'; import { RoleTypeRepository } from '@app/common/modules/role-type/repositories'; +import { InviteUserEntity } from '@app/common/modules/Invite-user/entities'; @Injectable() export class InviteUserService { @@ -188,76 +189,35 @@ 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 }, - }); + async activationCode(dto: ActivateCodeDto): Promise { + const { activationCode, userUuid } = dto; + + try { + const user = await this.getUser(userUuid); - if (!user) { - throw new HttpException('User not found', HttpStatus.NOT_FOUND); - } - const { email } = user; const invitedUser = await this.inviteUserRepository.findOne({ where: { - email, - invitationCode: activationCode, + email: user.email, status: UserStatusEnum.INVITED, isActive: true, }, relations: ['project', 'spaces.space.community', 'roleType'], }); - 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, + if (invitedUser) { + if (invitedUser.invitationCode !== activationCode) { + throw new HttpException( + 'Invalid activation code', + HttpStatus.BAD_REQUEST, ); - - 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 }, - roleType: { uuid: invitedUser.roleType.uuid }, - }, - ); + // 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, @@ -272,6 +232,73 @@ export class InviteUserService { ); } } + + 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 }, + ); + await this.userRepository.update( + { uuid: user.uuid }, + { + project: { uuid: invitedUser.project.uuid }, + inviteUser: { uuid: invitedUser.uuid }, + roleType: { uuid: invitedUser.roleType.uuid }, + }, + ); + } + async updateUserInvitation( dto: UpdateUserInvitationDto, invitedUserUuid: string, diff --git a/src/space-model/space-model.module.ts b/src/space-model/space-model.module.ts index 7f72860..c00a00d 100644 --- a/src/space-model/space-model.module.ts +++ b/src/space-model/space-model.module.ts @@ -20,6 +20,7 @@ import { } from './handlers'; import { CqrsModule } from '@nestjs/cqrs'; import { + InviteSpaceRepository, SpaceLinkRepository, SpaceRepository, TagRepository, @@ -70,6 +71,7 @@ const CommandHandlers = [ CommunityRepository, SpaceLinkService, SpaceLinkRepository, + InviteSpaceRepository, ], exports: [CqrsModule, SpaceModelService], }) diff --git a/src/space/controllers/space.controller.ts b/src/space/controllers/space.controller.ts index f54a3ea..bb18516 100644 --- a/src/space/controllers/space.controller.ts +++ b/src/space/controllers/space.controller.ts @@ -112,7 +112,6 @@ export class SpaceController { return this.spaceService.getSpacesHierarchyForSpace(params); } - //should it be post? @ApiBearerAuth() @UseGuards(PermissionsGuard) @Permissions('SPACE_MEMBER_ADD') @@ -121,10 +120,10 @@ export class SpaceController { description: ControllerRoute.SPACE.ACTIONS.CREATE_INVITATION_CODE_SPACE_DESCRIPTION, }) - @Get(':spaceUuid/invitation-code') + @Post(':spaceUuid/invitation-code') async generateSpaceInvitationCode( @Param() params: GetSpaceParam, ): Promise { - return this.spaceService.getSpaceInvitationCode(params); + return this.spaceService.generateSpaceInvitationCode(params); } } diff --git a/src/space/services/space.service.ts b/src/space/services/space.service.ts index d0394c6..87700e4 100644 --- a/src/space/services/space.service.ts +++ b/src/space/services/space.service.ts @@ -1,4 +1,7 @@ -import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { + InviteSpaceRepository, + SpaceRepository, +} from '@app/common/modules/space/repositories'; import { BadRequestException, HttpException, @@ -34,6 +37,7 @@ export class SpaceService { constructor( private readonly dataSource: DataSource, private readonly spaceRepository: SpaceRepository, + private readonly inviteSpaceRepository: InviteSpaceRepository, private readonly spaceLinkService: SpaceLinkService, private readonly subSpaceService: SubSpaceService, private readonly validationService: ValidationService, @@ -437,7 +441,7 @@ export class SpaceService { } } - async getSpaceInvitationCode(params: GetSpaceParam): Promise { + async generateSpaceInvitationCode(params: GetSpaceParam): Promise { const { communityUuid, spaceUuid, projectUuid } = params; try { const invitationCode = generateRandomString(6); @@ -448,13 +452,18 @@ export class SpaceService { projectUuid, spaceUuid, ); - - space.invitationCode = invitationCode; - await this.spaceRepository.save(space); + await this.inviteSpaceRepository.save({ + space: { uuid: spaceUuid }, + invitationCode, + }); return new SuccessResponseDto({ message: `Invitation code has been successfuly added to the space`, - data: space, + data: { + invitationCode, + spaceName: space.spaceName, + spaceUuid: space.uuid, + }, }); } catch (err) { if (err instanceof BadRequestException) { diff --git a/src/space/space.module.ts b/src/space/space.module.ts index f9a2317..e4bddfc 100644 --- a/src/space/space.module.ts +++ b/src/space/space.module.ts @@ -22,6 +22,7 @@ import { SpaceRepository, SpaceLinkRepository, TagRepository, + InviteSpaceRepository, } from '@app/common/modules/space/repositories'; import { CommunityRepository } from '@app/common/modules/community/repositories'; import { @@ -58,11 +59,14 @@ import { SubSpaceModelService, TagModelService, } from 'src/space-model/services'; -import { UserSpaceService } from 'src/users/services'; +import { UserService, UserSpaceService } from 'src/users/services'; import { UserDevicePermissionService } from 'src/user-device-permission/services'; import { PermissionTypeRepository } from '@app/common/modules/permission/repositories'; import { CqrsModule } from '@nestjs/cqrs'; import { DisableSpaceHandler } from './handlers'; +import { RegionRepository } from '@app/common/modules/region/repositories'; +import { TimeZoneRepository } from '@app/common/modules/timezone/repositories'; +import { InviteUserRepository } from '@app/common/modules/Invite-user/repositiories'; export const CommandHandlers = [DisableSpaceHandler]; @@ -114,7 +118,12 @@ export const CommandHandlers = [DisableSpaceHandler]; UserDevicePermissionService, DeviceUserPermissionRepository, PermissionTypeRepository, + InviteSpaceRepository, ...CommandHandlers, + UserService, + RegionRepository, + TimeZoneRepository, + InviteUserRepository, ], exports: [SpaceService], }) diff --git a/src/users/controllers/user-space.controller.ts b/src/users/controllers/user-space.controller.ts index aed945a..519a033 100644 --- a/src/users/controllers/user-space.controller.ts +++ b/src/users/controllers/user-space.controller.ts @@ -1,20 +1,11 @@ import { ControllerRoute } from '@app/common/constants/controller-route'; import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; -import { - Body, - Controller, - Get, - HttpException, - HttpStatus, - Param, - Post, - UseGuards, -} from '@nestjs/common'; +import { Controller, Get, Param, UseGuards } from '@nestjs/common'; import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; import { UserSpaceService } from '../services'; import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; import { BaseResponseDto } from '@app/common/dto/base.response.dto'; -import { AddUserSpaceUsingCodeDto, UserParamDto } from '../dtos'; +import { UserParamDto } from '../dtos'; @ApiTags('User Module') @Controller({ @@ -36,36 +27,4 @@ export class UserSpaceController { ): Promise { return this.userSpaceService.getSpacesForUser(params.userUuid); } - - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - @Post('/verify-code') - @ApiOperation({ - summary: - ControllerRoute.USER_SPACE.ACTIONS.VERIFY_CODE_AND_ADD_USER_SPACE_SUMMARY, - description: - ControllerRoute.USER_SPACE.ACTIONS - .VERIFY_CODE_AND_ADD_USER_SPACE_DESCRIPTION, - }) - async verifyCodeAndAddUserSpace( - @Body() dto: AddUserSpaceUsingCodeDto, - @Param() params: UserParamDto, - ) { - try { - await this.userSpaceService.verifyCodeAndAddUserSpace( - dto, - params.userUuid, - ); - return { - statusCode: HttpStatus.CREATED, - success: true, - message: 'user space added successfully', - }; - } catch (error) { - throw new HttpException( - error.message || 'Internal server error', - 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 71611bc..074dcf1 100644 --- a/src/users/services/user-space.service.ts +++ b/src/users/services/user-space.service.ts @@ -1,19 +1,34 @@ -import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +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 { SpaceRepository } from '@app/common/modules/space/repositories'; +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 { SpaceEntity } from '@app/common/modules/space/entities'; +import { InviteSpaceEntity } from '@app/common/modules/space/entities/invite-space.entity'; +import { UserService } from './user.service'; +import { RoleType } from '@app/common/constants/role.type.enum'; +import { InviteUserRepository } from '@app/common/modules/Invite-user/repositiories'; +import { UserStatusEnum } from '@app/common/constants/user-status.enum'; @Injectable() export class UserSpaceService { constructor( private readonly userSpaceRepository: UserSpaceRepository, private readonly spaceRepository: SpaceRepository, + private readonly inviteSpaceRepository: InviteSpaceRepository, + private readonly userService: UserService, + private readonly inviteUserRepository: InviteUserRepository, private readonly userDevicePermissionService: UserDevicePermissionService, ) {} @@ -37,16 +52,25 @@ export class UserSpaceService { params: AddUserSpaceUsingCodeDto, userUuid: string, ) { + const { inviteCode } = params; try { - const space = await this.findSpaceByInviteCode(params.inviteCode); + const inviteSpace = await this.findInviteSpaceByInviteCode(inviteCode); + const user = await this.userService.getUserDetailsByUserUuid( + userUuid, + true, + ); + await this.checkSpaceMemberRole(user); + await this.addUserToSpace(userUuid, inviteSpace.space.uuid); - await this.addUserToSpace(userUuid, space.uuid); - - await this.clearUnitInvitationCode(space.uuid); - - const deviceUUIDs = await this.getDeviceUUIDsForSpace(space.uuid); + const deviceUUIDs = await this.getDeviceUUIDsForSpace(inviteSpace.uuid); await this.addUserPermissionsToDevices(userUuid, deviceUUIDs); + await this.addUserAsActiveInvitation( + user, + inviteSpace.space.uuid, + inviteCode, + ); + await this.clearSpaceInvitationCode(inviteSpace.uuid); } catch (err) { if (err instanceof HttpException) { throw err; @@ -58,25 +82,86 @@ export class UserSpaceService { } } } - - private async findSpaceByInviteCode( - inviteCode: string, - ): Promise { + private async checkSpaceMemberRole(user: any) { try { - const space = await this.spaceRepository.findOneOrFail({ - where: { - invitationCode: inviteCode, - }, - }); - return space; - } catch (error) { + if (user.role.type !== RoleType.SPACE_MEMBER) { + throw new BadRequestException( + 'You have to be a space member to join this space', + ); + } + } catch (err) { throw new HttpException( - 'Space with the provided invite code not found', - HttpStatus.NOT_FOUND, + err.message || 'User not found', + err.status || HttpStatus.NOT_FOUND, ); } } + private async findInviteSpaceByInviteCode( + inviteCode: string, + ): Promise { + try { + const inviteSpace = await this.inviteSpaceRepository.findOneOrFail({ + where: { + invitationCode: inviteCode, + isActive: true, + }, + relations: ['space'], + }); + return inviteSpace; + } catch (error) { + throw new HttpException( + 'Invalid invitation code', + HttpStatus.BAD_REQUEST, + ); + } + } + private async clearSpaceInvitationCode(inviteSpaceUuid: string) { + await this.inviteSpaceRepository.update( + { uuid: inviteSpaceUuid }, + { isActive: false }, + ); + } + async getProjectBySpaceUuid(spaceUuid: string) { + try { + const project = await this.spaceRepository.findOne({ + where: { + uuid: spaceUuid, + }, + relations: ['community.project'], + }); + return project; + } catch (error) { + throw new HttpException('Space not found', HttpStatus.NOT_FOUND); + } + } + private async addUserAsActiveInvitation( + user: any, + spaceUuid: string, + inviteCode: string, + ) { + try { + const space = await this.getProjectBySpaceUuid(spaceUuid); + const inviteUser = this.inviteUserRepository.create({ + firstName: user.firstName, + lastName: user.lastName, + email: user.email, + jobTitle: null, + phoneNumber: null, + roleType: { uuid: user.role.uuid }, + status: UserStatusEnum.ACTIVE, + invitationCode: inviteCode, + invitedBy: RoleType.SPACE_OWNER, + project: { uuid: space.community.project.uuid }, + }); + await this.inviteUserRepository.save(inviteUser); + } catch (err) { + throw new HttpException( + err.message || 'Internal Server Error', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } private async addUserToSpace(userUuid: string, spaceUuid: string) { try { const user = await this.addUserSpace({ userUuid, spaceUuid }); @@ -110,13 +195,6 @@ export class UserSpaceService { } } - private async clearUnitInvitationCode(spaceUuid: string) { - await this.spaceRepository.update( - { uuid: spaceUuid }, - { invitationCode: null }, - ); - } - async getDeviceUUIDsForSpace(unitUuid: string): Promise<{ uuid: string }[]> { const devices = await this.spaceRepository.find({ where: { uuid: unitUuid }, diff --git a/src/users/services/user.service.ts b/src/users/services/user.service.ts index 80dad24..96a669b 100644 --- a/src/users/services/user.service.ts +++ b/src/users/services/user.service.ts @@ -23,13 +23,15 @@ export class UserService { private readonly regionRepository: RegionRepository, private readonly timeZoneRepository: TimeZoneRepository, ) {} - async getUserDetailsByUserUuid(userUuid: string) { + async getUserDetailsByUserUuid(userUuid: string, withRole = false) { try { const user = await this.userRepository.findOne({ where: { uuid: userUuid, }, - relations: ['region', 'timezone'], + ...(withRole + ? { relations: ['roleType'] } + : { relations: ['region', 'timezone'] }), }); if (!user) { throw new BadRequestException('Invalid room UUID'); @@ -39,13 +41,14 @@ export class UserService { const cleanedProfilePicture = removeBase64Prefix(user.profilePicture); return { - uuid: user.uuid, - email: user.email, - firstName: user.firstName, - lastName: user.lastName, + uuid: user?.uuid, + email: user?.email, + firstName: user?.firstName, + lastName: user?.lastName, profilePicture: cleanedProfilePicture, - region: user.region, - timeZone: user.timezone, + region: user?.region, + timeZone: user?.timezone, + ...(withRole && { role: user?.roleType }), }; } catch (err) { if (err instanceof BadRequestException) { diff --git a/src/users/user.module.ts b/src/users/user.module.ts index 20bd06f..bf5164d 100644 --- a/src/users/user.module.ts +++ b/src/users/user.module.ts @@ -12,10 +12,14 @@ import { UserSpaceController } from './controllers'; import { CommunityModule } from 'src/community/community.module'; import { UserSpaceService } from './services'; import { CommunityRepository } from '@app/common/modules/community/repositories'; -import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { + InviteSpaceRepository, + SpaceRepository, +} from '@app/common/modules/space/repositories'; 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 { InviteUserRepository } from '@app/common/modules/Invite-user/repositiories'; @Module({ imports: [ConfigModule, CommunityModule], @@ -32,6 +36,8 @@ import { PermissionTypeRepository } from '@app/common/modules/permission/reposit DeviceUserPermissionRepository, PermissionTypeRepository, UserSpaceService, + InviteSpaceRepository, + InviteUserRepository, ], exports: [UserService], })