diff --git a/libs/common/src/constants/controller-route.ts b/libs/common/src/constants/controller-route.ts index 10e20d9..c26deca 100644 --- a/libs/common/src/constants/controller-route.ts +++ b/libs/common/src/constants/controller-route.ts @@ -721,4 +721,24 @@ export class ControllerRoute { 'This endpoint deletes a user’s subscription for device messages.'; }; }; + static INVITE_USER = class { + public static readonly ROUTE = 'invite-user'; + static ACTIONS = class { + public static readonly CREATE_USER_INVITATION_SUMMARY = + 'Create user invitation'; + + public static readonly CREATE_USER_INVITATION_DESCRIPTION = + 'This endpoint creates an invitation for a user to assign to role and spaces.'; + }; + }; + static PERMISSION = class { + public static readonly ROUTE = 'permission'; + static ACTIONS = class { + public static readonly GET_PERMISSION_BY_ROLE_SUMMARY = + 'Get permissions by role'; + + public static readonly GET_PERMISSION_BY_ROLE_DESCRIPTION = + 'This endpoint retrieves the permissions associated with a specific role.'; + }; + }; } diff --git a/libs/common/src/constants/user-status.enum.ts b/libs/common/src/constants/user-status.enum.ts new file mode 100644 index 0000000..b0b9817 --- /dev/null +++ b/libs/common/src/constants/user-status.enum.ts @@ -0,0 +1,5 @@ +export enum UserStatusEnum { + ACTIVE = 'active', + INVITED = 'invited', + DISABLED = 'disabled', +} diff --git a/libs/common/src/modules/Invite-user/Invite-user.repository.module.ts b/libs/common/src/modules/Invite-user/Invite-user.repository.module.ts new file mode 100644 index 0000000..4cc8596 --- /dev/null +++ b/libs/common/src/modules/Invite-user/Invite-user.repository.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { InviteUserEntity, InviteUserSpaceEntity } from './entities'; + +@Module({ + providers: [], + exports: [], + controllers: [], + imports: [ + TypeOrmModule.forFeature([InviteUserEntity, InviteUserSpaceEntity]), + ], +}) +export class InviteUserRepositoryModule {} 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 new file mode 100644 index 0000000..d7db8dc --- /dev/null +++ b/libs/common/src/modules/Invite-user/dtos/Invite-user.dto.ts @@ -0,0 +1,42 @@ +import { UserStatusEnum } from '@app/common/constants/user-status.enum'; +import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; + +export class InviteUserDto { + @IsString() + @IsNotEmpty() + public uuid: string; + + @IsString() + @IsNotEmpty() + public email: string; + @IsString() + @IsNotEmpty() + public jobTitle: string; + @IsEnum(UserStatusEnum) + @IsNotEmpty() + public status: UserStatusEnum; + @IsString() + @IsNotEmpty() + public firstName: string; + + @IsString() + @IsNotEmpty() + public lastName: string; +} +export class InviteUserSpaceDto { + @IsString() + @IsNotEmpty() + public uuid: string; + + @IsString() + @IsNotEmpty() + public inviteUserUuid: string; + + @IsString() + @IsNotEmpty() + public spaceUuid: string; + + @IsString() + @IsNotEmpty() + public invitationCode: string; +} diff --git a/libs/common/src/modules/Invite-user/dtos/index.ts b/libs/common/src/modules/Invite-user/dtos/index.ts new file mode 100644 index 0000000..2385037 --- /dev/null +++ b/libs/common/src/modules/Invite-user/dtos/index.ts @@ -0,0 +1 @@ +export * from './Invite-user.dto'; 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 new file mode 100644 index 0000000..06e5105 --- /dev/null +++ b/libs/common/src/modules/Invite-user/entities/Invite-user.entity.ts @@ -0,0 +1,112 @@ +import { + Column, + Entity, + JoinColumn, + ManyToOne, + OneToMany, + OneToOne, + Unique, +} from 'typeorm'; +import { InviteUserDto, InviteUserSpaceDto } from '../dtos'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { RoleTypeEntity } from '../../role-type/entities'; +import { UserStatusEnum } from '@app/common/constants/user-status.enum'; +import { UserEntity } from '../../user/entities'; +import { SpaceEntity } from '../../space/entities'; + +@Entity({ name: 'invite-user' }) +@Unique(['email', 'invitationCode']) +export class InviteUserEntity extends AbstractEntity { + @Column({ + type: 'uuid', + default: () => 'gen_random_uuid()', + nullable: false, + }) + public uuid: string; + + @Column({ + nullable: false, + unique: true, + }) + email: string; + + @Column({ + nullable: false, + }) + jobTitle: string; + + @Column({ + nullable: false, + enum: Object.values(UserStatusEnum), + }) + status: string; + + @Column() + public firstName: string; + + @Column({ + nullable: false, + }) + public lastName: string; + @Column({ + nullable: false, + }) + public phoneNumber: string; + + @Column({ + nullable: false, + default: true, + }) + public isEnabled: boolean; + + @Column({ + nullable: false, + default: true, + }) + public isActive: boolean; + @Column({ + nullable: false, + unique: true, + }) + public invitationCode: string; + // Relation with RoleTypeEntity + @ManyToOne(() => RoleTypeEntity, (roleType) => roleType.invitedUsers, { + nullable: false, + onDelete: 'CASCADE', + }) + public roleType: RoleTypeEntity; + @OneToOne(() => UserEntity, (user) => user.inviteUser, { nullable: true }) + @JoinColumn({ name: 'user_uuid' }) // Foreign key in InviteUserEntity + user: UserEntity; + @OneToMany( + () => InviteUserSpaceEntity, + (inviteUserSpace) => inviteUserSpace.inviteUser, + ) + spaces: InviteUserSpaceEntity[]; + constructor(partial: Partial) { + super(); + Object.assign(this, partial); + } +} +@Entity({ name: 'invite-user-space' }) +@Unique(['inviteUser', 'space']) +export class InviteUserSpaceEntity extends AbstractEntity { + @Column({ + type: 'uuid', + default: () => 'gen_random_uuid()', + nullable: false, + }) + public uuid: string; + + @ManyToOne(() => InviteUserEntity, (inviteUser) => inviteUser.spaces) + @JoinColumn({ name: 'invite_user_uuid' }) + public inviteUser: InviteUserEntity; + + @ManyToOne(() => SpaceEntity, (space) => space.invitedUsers) + @JoinColumn({ name: 'space_uuid' }) + public space: SpaceEntity; + constructor(partial: Partial) { + super(); + Object.assign(this, partial); + } +} diff --git a/libs/common/src/modules/Invite-user/entities/index.ts b/libs/common/src/modules/Invite-user/entities/index.ts new file mode 100644 index 0000000..3f0da22 --- /dev/null +++ b/libs/common/src/modules/Invite-user/entities/index.ts @@ -0,0 +1 @@ +export * from './Invite-user.entity'; diff --git a/libs/common/src/modules/Invite-user/repositories/Invite-user.repository.ts b/libs/common/src/modules/Invite-user/repositories/Invite-user.repository.ts new file mode 100644 index 0000000..3df7390 --- /dev/null +++ b/libs/common/src/modules/Invite-user/repositories/Invite-user.repository.ts @@ -0,0 +1,16 @@ +import { DataSource, Repository } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { InviteUserEntity, InviteUserSpaceEntity } from '../entities/'; + +@Injectable() +export class InviteUserRepository extends Repository { + constructor(private dataSource: DataSource) { + super(InviteUserEntity, dataSource.createEntityManager()); + } +} +@Injectable() +export class InviteUserSpaceRepository extends Repository { + constructor(private dataSource: DataSource) { + super(InviteUserSpaceEntity, dataSource.createEntityManager()); + } +} diff --git a/libs/common/src/modules/Invite-user/repositories/index.ts b/libs/common/src/modules/Invite-user/repositories/index.ts new file mode 100644 index 0000000..a60f19a --- /dev/null +++ b/libs/common/src/modules/Invite-user/repositories/index.ts @@ -0,0 +1 @@ +export * from './Invite-user.repository'; diff --git a/libs/common/src/modules/space/entities/space.entity.ts b/libs/common/src/modules/space/entities/space.entity.ts index 103f0a2..cb8367c 100644 --- a/libs/common/src/modules/space/entities/space.entity.ts +++ b/libs/common/src/modules/space/entities/space.entity.ts @@ -15,6 +15,7 @@ import { SubspaceEntity } from './subspace.entity'; import { SpaceLinkEntity } from './space-link.entity'; import { SpaceProductEntity } from './space-product.entity'; import { SceneEntity } from '../../scene/entities'; +import { InviteUserSpaceEntity } from '../../Invite-user/entities'; @Entity({ name: 'space' }) @Unique(['invitationCode']) @@ -97,7 +98,11 @@ export class SpaceEntity extends AbstractEntity { @OneToMany(() => SceneEntity, (scene) => scene.space) scenes: SceneEntity[]; - + @OneToMany( + () => InviteUserSpaceEntity, + (inviteUserSpace) => inviteUserSpace.space, + ) + invitedUsers: InviteUserSpaceEntity[]; constructor(partial: Partial) { super(); Object.assign(this, partial); diff --git a/libs/common/src/modules/user/dtos/user.dto.ts b/libs/common/src/modules/user/dtos/user.dto.ts index 0a4bda2..01c0c24 100644 --- a/libs/common/src/modules/user/dtos/user.dto.ts +++ b/libs/common/src/modules/user/dtos/user.dto.ts @@ -58,20 +58,6 @@ export class UserOtpDto { public expiryTime: string; } -export class UserRoleDto { - @IsString() - @IsNotEmpty() - public uuid: string; - - @IsString() - @IsNotEmpty() - public userUuid: string; - - @IsString() - @IsNotEmpty() - public roleTypeUuid: string; -} - export class UserSpaceDto { @IsString() @IsNotEmpty() diff --git a/libs/common/src/modules/user/entities/user.entity.ts b/libs/common/src/modules/user/entities/user.entity.ts index c9a11e6..04697fa 100644 --- a/libs/common/src/modules/user/entities/user.entity.ts +++ b/libs/common/src/modules/user/entities/user.entity.ts @@ -2,15 +2,16 @@ import { Column, DeleteDateColumn, Entity, + JoinColumn, ManyToOne, OneToMany, + OneToOne, Unique, } from 'typeorm'; import { UserDto, UserNotificationDto, UserOtpDto, - UserRoleDto, UserSpaceDto, } from '../dtos'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; @@ -26,6 +27,8 @@ import { OtpType } from '../../../../src/constants/otp-type.enum'; import { RoleTypeEntity } from '../../role-type/entities'; import { SpaceEntity } from '../../space/entities'; import { VisitorPasswordEntity } from '../../visitor-password/entities'; +import { InviteUserEntity } from '../../Invite-user/entities'; +import { ProjectEntity } from '../../project/entities'; @Entity({ name: 'user' }) export class UserEntity extends AbstractEntity { @@ -100,10 +103,7 @@ export class UserEntity extends AbstractEntity { (deviceUserNotification) => deviceUserNotification.user, ) deviceUserNotification: DeviceNotificationEntity[]; - @OneToMany(() => UserRoleEntity, (role) => role.user, { - nullable: true, - }) - roles: UserRoleEntity[]; + @ManyToOne(() => RegionEntity, (region) => region.users, { nullable: true }) region: RegionEntity; @ManyToOne(() => TimeZoneEntity, (timezone) => timezone.users, { @@ -116,6 +116,21 @@ export class UserEntity extends AbstractEntity { ) public visitorPasswords: VisitorPasswordEntity[]; + @ManyToOne(() => RoleTypeEntity, (roleType) => roleType.users, { + nullable: true, + }) + public roleType: RoleTypeEntity; + @OneToOne(() => InviteUserEntity, (inviteUser) => inviteUser.user, { + nullable: true, + }) + @JoinColumn({ name: 'invite_user_uuid' }) + inviteUser: InviteUserEntity; + + @ManyToOne(() => ProjectEntity, (project) => project.users, { + nullable: true, + }) + @JoinColumn({ name: 'project_uuid' }) + public project: ProjectEntity; constructor(partial: Partial) { super(); Object.assign(this, partial); @@ -125,7 +140,7 @@ export class UserEntity extends AbstractEntity { @Entity({ name: 'user-notification' }) @Unique(['user', 'subscriptionUuid']) export class UserNotificationEntity extends AbstractEntity { - @ManyToOne(() => UserEntity, (user) => user.roles, { + @ManyToOne(() => UserEntity, (user) => user.roleType, { nullable: false, }) user: UserEntity; @@ -178,25 +193,6 @@ export class UserOtpEntity extends AbstractEntity { } } -@Entity({ name: 'user-role' }) -@Unique(['user', 'roleType']) -export class UserRoleEntity extends AbstractEntity { - @ManyToOne(() => UserEntity, (user) => user.roles, { - nullable: false, - }) - user: UserEntity; - - @ManyToOne(() => RoleTypeEntity, (roleType) => roleType.roles, { - nullable: false, - }) - roleType: RoleTypeEntity; - - constructor(partial: Partial) { - super(); - Object.assign(this, partial); - } -} - @Entity({ name: 'user-space' }) @Unique(['user', 'space']) export class UserSpaceEntity extends AbstractEntity { diff --git a/libs/common/src/modules/user/repositories/user.repository.ts b/libs/common/src/modules/user/repositories/user.repository.ts index ffc1aa1..d2303f9 100644 --- a/libs/common/src/modules/user/repositories/user.repository.ts +++ b/libs/common/src/modules/user/repositories/user.repository.ts @@ -4,7 +4,6 @@ import { UserEntity, UserNotificationEntity, UserOtpEntity, - UserRoleEntity, UserSpaceEntity, } from '../entities/'; @@ -29,13 +28,6 @@ export class UserOtpRepository extends Repository { } } -@Injectable() -export class UserRoleRepository extends Repository { - constructor(private dataSource: DataSource) { - super(UserRoleEntity, dataSource.createEntityManager()); - } -} - @Injectable() export class UserSpaceRepository extends Repository { constructor(private dataSource: DataSource) { diff --git a/libs/common/src/seed/seeder.module.ts b/libs/common/src/seed/seeder.module.ts index eeb245b..6de5585 100644 --- a/libs/common/src/seed/seeder.module.ts +++ b/libs/common/src/seed/seeder.module.ts @@ -10,7 +10,6 @@ import { RoleTypeSeeder } from './services/role.type.seeder'; import { SpaceRepositoryModule } from '../modules/space/space.repository.module'; import { SuperAdminSeeder } from './services/supper.admin.seeder'; import { UserRepository } from '../modules/user/repositories'; -import { UserRoleRepository } from '../modules/user/repositories'; import { UserRepositoryModule } from '../modules/user/user.repository.module'; import { RegionSeeder } from './services/regions.seeder'; import { RegionRepository } from '../modules/region/repositories'; @@ -28,7 +27,6 @@ import { SceneIconRepository } from '../modules/scene/repositories'; RoleTypeRepository, SuperAdminSeeder, UserRepository, - UserRoleRepository, RegionSeeder, RegionRepository, TimeZoneSeeder, diff --git a/src/invite-user/services/invite-user.service.ts b/src/invite-user/services/invite-user.service.ts new file mode 100644 index 0000000..931c163 --- /dev/null +++ b/src/invite-user/services/invite-user.service.ts @@ -0,0 +1,103 @@ +import { InviteUserSpaceRepository } from './../../../libs/common/src/modules/Invite-user/repositories/Invite-user.repository'; +import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; +import { AddUserInvitationDto } from '../dtos'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { InviteUserRepository } from '@app/common/modules/Invite-user/repositories'; +import { UserStatusEnum } from '@app/common/constants/user-status.enum'; +import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; +import { generateRandomString } from '@app/common/helper/randomString'; +import { IsNull, Not } from 'typeorm'; +import { DataSource } from 'typeorm'; +import { UserEntity } from '@app/common/modules/user/entities'; + +@Injectable() +export class InviteUserService { + constructor( + private readonly inviteUserRepository: InviteUserRepository, + private readonly inviteUserSpaceRepository: InviteUserSpaceRepository, + private readonly dataSource: DataSource, + ) {} + + async createUserInvitation( + dto: AddUserInvitationDto, + ): Promise { + const { + firstName, + lastName, + email, + jobTitle, + phoneNumber, + roleUuid, + spaceUuids, + } = dto; + + const invitationCode = generateRandomString(6); + const queryRunner = this.dataSource.createQueryRunner(); + + await queryRunner.startTransaction(); + + try { + const userRepo = queryRunner.manager.getRepository(UserEntity); + + const user = await userRepo.findOne({ + where: { + email, + project: Not(IsNull()), + }, + }); + + if (user) { + throw new HttpException( + 'User already has a project', + HttpStatus.BAD_REQUEST, + ); + } + + const inviteUser = this.inviteUserRepository.create({ + firstName, + lastName, + email, + jobTitle, + phoneNumber, + roleType: { uuid: roleUuid }, + status: UserStatusEnum.INVITED, + invitationCode, + }); + + const invitedUser = await queryRunner.manager.save(inviteUser); + + const spacePromises = spaceUuids.map(async (spaceUuid) => { + const inviteUserSpace = this.inviteUserSpaceRepository.create({ + inviteUser: { uuid: invitedUser.uuid }, + space: { uuid: spaceUuid }, + }); + return queryRunner.manager.save(inviteUserSpace); + }); + + await Promise.all(spacePromises); + + await queryRunner.commitTransaction(); + + return new SuccessResponseDto({ + statusCode: HttpStatus.CREATED, + success: true, + data: { + invitationCode: invitedUser.invitationCode, + }, + message: 'User invited successfully', + }); + } catch (error) { + await queryRunner.rollbackTransaction(); + if (error instanceof HttpException) { + throw error; + } + console.error('Error creating user invitation:', error); + throw new HttpException( + error.message || 'An unexpected error occurred while inviting the user', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } finally { + await queryRunner.release(); + } + } +}