Add permission and role management features

This commit is contained in:
faris Aljohari
2024-12-16 00:19:14 -06:00
parent 57397e653a
commit 64027d3a16
22 changed files with 473 additions and 17 deletions

View File

@ -0,0 +1,43 @@
export const PermissionMapping = {
DEVICE_MANAGEMENT: {
DEVICE: ['SINGLE_CONTROL', 'VIEW', 'DELETE', 'UPDATE', 'BATCH_CONTROL'],
FIRMWARE: ['CONTROL', 'VIEW'],
},
COMMUNITY_MANAGEMENT: {
COMMUNITY: ['VIEW', 'ADD', 'UPDATE', 'DELETE'],
},
SPACE_MANAGEMENT: {
SPACE: [
'VIEW',
'ADD',
'UPDATE',
'DELETE',
'MODULE_ADD',
'ASSIGN_USER_TO_SPACE',
'DELETE_USER_FROM_SPACE',
],
SUBSPACE: [
'VIEW',
'ADD',
'UPDATE',
'DELETE',
'ASSIGN_DEVICE_TO_SUBSPACE',
'DELETE_DEVICE_FROM_SUBSPACE',
],
},
DEVICE_WIZARD: {
DEVICE_WIZARD: ['VIEW_DEVICE_WIZARD'],
SPACE_DEVICE: ['VIEW_DEVICE_IN_SPACE', 'ASSIGN_DEVICE_TO_SPACE'],
SUBSPACE_DEVICE: ['VIEW_DEVICE_IN_SUBSPACE', 'UPDATE_DEVICE_IN_SUBSPACE'],
},
AUTOMATION_MANAGEMENT: {
AUTOMATION: ['VIEW', 'ADD', 'UPDATE', 'DELETE', 'CONTROL'],
SCENES: ['VIEW', 'ADD', 'UPDATE', 'DELETE', 'CONTROL'],
},
VISITOR_PASSWORD_MANAGEMENT: {
VISITOR_PASSWORD: ['VIEW', 'ADD', 'UPDATE', 'DELETE'],
},
USER_MANAGEMENT: {
USER: ['ADD'],
},
};

View File

@ -0,0 +1,130 @@
import { RoleType } from './role.type.enum';
export const RolePermissions = {
[RoleType.SUPER_ADMIN]: [
'DEVICE_SINGLE_CONTROL',
'DEVICE_VIEW',
'DEVICE_DELETE',
'DEVICE_UPDATE',
'DEVICE_BATCH_CONTROL',
'COMMUNITY_VIEW',
'COMMUNITY_ADD',
'COMMUNITY_UPDATE',
'COMMUNITY_DELETE',
'FIRMWARE_CONTROL',
'SPACE_VIEW',
'SPACE_ADD',
'SPACE_UPDATE',
'SPACE_DELETE',
'SPACE_MODULE_ADD',
'ASSIGN_USER_TO_SPACE',
'DELETE_USER_FROM_SPACE',
'SUBSPACE_VIEW',
'SUBSPACE_ADD',
'SUBSPACE_UPDATE',
'SUBSPACE_DELETE',
'ASSIGN_DEVICE_TO_SUBSPACE',
'DELETE_DEVICE_FROM_SUBSPACE',
'VIEW_DEVICE_WIZARD',
'VIEW_DEVICE_IN_SUBSPACE',
'VIEW_DEVICE_IN_SPACE',
'UPDATE_DEVICE_IN_SUBSPACE',
'ASSIGN_DEVICE_TO_SPACE',
'AUTOMATION_VIEW',
'AUTOMATION_ADD',
'AUTOMATION_UPDATE',
'AUTOMATION_DELETE',
'AUTOMATION_CONTROL',
'SCENES_VIEW',
'SCENES_ADD',
'SCENES_UPDATE',
'SCENES_DELETE',
'SCENES_CONTROL',
'VISITOR_PASSWORD_VIEW',
'VISITOR_PASSWORD_ADD',
'USER_ADD',
],
[RoleType.ADMIN]: [
'DEVICE_SINGLE_CONTROL',
'DEVICE_VIEW',
'DEVICE_DELETE',
'DEVICE_UPDATE',
'DEVICE_BATCH_CONTROL',
'COMMUNITY_VIEW',
'COMMUNITY_ADD',
'COMMUNITY_UPDATE',
'COMMUNITY_DELETE',
'FIRMWARE_CONTROL',
'SPACE_VIEW',
'SPACE_ADD',
'SPACE_UPDATE',
'SPACE_DELETE',
'SPACE_MODULE_ADD',
'ASSIGN_USER_TO_SPACE',
'DELETE_USER_FROM_SPACE',
'SUBSPACE_VIEW',
'SUBSPACE_ADD',
'SUBSPACE_UPDATE',
'SUBSPACE_DELETE',
'ASSIGN_DEVICE_TO_SUBSPACE',
'DELETE_DEVICE_FROM_SUBSPACE',
'VIEW_DEVICE_WIZARD',
'VIEW_DEVICE_IN_SUBSPACE',
'VIEW_DEVICE_IN_SPACE',
'UPDATE_DEVICE_IN_SUBSPACE',
'ASSIGN_DEVICE_TO_SPACE',
'AUTOMATION_VIEW',
'AUTOMATION_ADD',
'AUTOMATION_UPDATE',
'AUTOMATION_DELETE',
'AUTOMATION_CONTROL',
'SCENES_VIEW',
'SCENES_ADD',
'SCENES_UPDATE',
'SCENES_DELETE',
'SCENES_CONTROL',
'VISITOR_PASSWORD_VIEW',
'VISITOR_PASSWORD_ADD',
'USER_ADD',
],
[RoleType.SPACE_MEMBER]: [
'DEVICE_SINGLE_CONTROL',
'DEVICE_VIEW',
'SPACE_VIEW',
'SUBSPACE_VIEW',
'VIEW_DEVICE_WIZARD',
'VIEW_DEVICE_IN_SUBSPACE',
'VIEW_DEVICE_IN_SPACE',
'AUTOMATION_VIEW',
'AUTOMATION_CONTROL',
'SCENES_VIEW',
'SCENES_CONTROL',
'VISITOR_PASSWORD_VIEW',
],
[RoleType.SPACE_OWNER]: [
'DEVICE_SINGLE_CONTROL',
'DEVICE_VIEW',
'FIRMWARE_CONTROL',
'FIRMWARE_VIEW',
'SPACE_VIEW',
'SPACE_MEMBER_ADD',
'SUBSPACE_VIEW',
'SUBSPACE_ADD',
'SUBSPACE_UPDATE',
'SUBSPACE_DELETE',
'AUTOMATION_VIEW',
'AUTOMATION_ADD',
'AUTOMATION_UPDATE',
'AUTOMATION_DELETE',
'AUTOMATION_CONTROL',
'SCENES_VIEW',
'SCENES_ADD',
'SCENES_UPDATE',
'SCENES_DELETE',
'SCENES_CONTROL',
'VISITOR_PASSWORD_VIEW',
'VISITOR_PASSWORD_ADD',
'VISITOR_PASSWORD_UPDATE',
'VISITOR_PASSWORD_DELETE',
],
};

View File

@ -15,7 +15,6 @@ import {
} from '../modules/space/entities'; } from '../modules/space/entities';
import { UserSpaceEntity } from '../modules/user/entities'; import { UserSpaceEntity } from '../modules/user/entities';
import { DeviceUserPermissionEntity } from '../modules/device/entities'; import { DeviceUserPermissionEntity } from '../modules/device/entities';
import { UserRoleEntity } from '../modules/user/entities';
import { RoleTypeEntity } from '../modules/role-type/entities'; import { RoleTypeEntity } from '../modules/role-type/entities';
import { UserNotificationEntity } from '../modules/user/entities'; import { UserNotificationEntity } from '../modules/user/entities';
import { DeviceNotificationEntity } from '../modules/device/entities'; import { DeviceNotificationEntity } from '../modules/device/entities';
@ -34,6 +33,10 @@ import {
SpaceProductModelEntity, SpaceProductModelEntity,
SubspaceModelEntity, SubspaceModelEntity,
} from '../modules/space-model/entities'; } from '../modules/space-model/entities';
import {
InviteUserEntity,
InviteUserSpaceEntity,
} from '../modules/Invite-user/entities';
@Module({ @Module({
imports: [ imports: [
TypeOrmModule.forRootAsync({ TypeOrmModule.forRootAsync({
@ -63,7 +66,6 @@ import {
SpaceProductEntity, SpaceProductEntity,
UserSpaceEntity, UserSpaceEntity,
DeviceUserPermissionEntity, DeviceUserPermissionEntity,
UserRoleEntity,
RoleTypeEntity, RoleTypeEntity,
UserNotificationEntity, UserNotificationEntity,
DeviceNotificationEntity, DeviceNotificationEntity,
@ -78,6 +80,8 @@ import {
SpaceProductModelEntity, SpaceProductModelEntity,
SpaceProductItemModelEntity, SpaceProductItemModelEntity,
SubspaceModelEntity, SubspaceModelEntity,
InviteUserEntity,
InviteUserSpaceEntity,
], ],
namingStrategy: new SnakeNamingStrategy(), namingStrategy: new SnakeNamingStrategy(),
synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))), synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))),

View File

@ -3,6 +3,7 @@ import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { ProjectDto } from '../dtos'; import { ProjectDto } from '../dtos';
import { CommunityEntity } from '../../community/entities'; import { CommunityEntity } from '../../community/entities';
import { SpaceModelEntity } from '../../space-model'; import { SpaceModelEntity } from '../../space-model';
import { UserEntity } from '../../user/entities';
@Entity({ name: 'project' }) @Entity({ name: 'project' })
@Unique(['name']) @Unique(['name'])
@ -28,6 +29,9 @@ export class ProjectEntity extends AbstractEntity<ProjectDto> {
@OneToMany(() => CommunityEntity, (community) => community.project) @OneToMany(() => CommunityEntity, (community) => community.project)
communities: CommunityEntity[]; communities: CommunityEntity[];
@OneToMany(() => UserEntity, (user) => user.project)
public users: UserEntity[];
constructor(partial: Partial<ProjectEntity>) { constructor(partial: Partial<ProjectEntity>) {
super(); super();
Object.assign(this, partial); Object.assign(this, partial);

View File

@ -2,7 +2,8 @@ import { Column, Entity, OneToMany, Unique } from 'typeorm';
import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { RoleTypeDto } from '../dtos/role.type.dto'; import { RoleTypeDto } from '../dtos/role.type.dto';
import { RoleType } from '@app/common/constants/role.type.enum'; import { RoleType } from '@app/common/constants/role.type.enum';
import { UserRoleEntity } from '../../user/entities'; import { UserEntity } from '../../user/entities';
import { InviteUserEntity } from '../../Invite-user/entities';
@Entity({ name: 'role-type' }) @Entity({ name: 'role-type' })
@Unique(['type']) @Unique(['type'])
@ -12,10 +13,14 @@ export class RoleTypeEntity extends AbstractEntity<RoleTypeDto> {
enum: Object.values(RoleType), enum: Object.values(RoleType),
}) })
type: string; type: string;
@OneToMany(() => UserRoleEntity, (role) => role.roleType, { @OneToMany(() => UserEntity, (inviteUser) => inviteUser.roleType, {
nullable: true, nullable: true,
}) })
roles: UserRoleEntity[]; users: UserEntity[];
@OneToMany(() => InviteUserEntity, (inviteUser) => inviteUser.roleType, {
nullable: true,
})
invitedUsers: InviteUserEntity[];
constructor(partial: Partial<RoleTypeEntity>) { constructor(partial: Partial<RoleTypeEntity>) {
super(); super();
Object.assign(this, partial); Object.assign(this, partial);

View File

@ -4,7 +4,6 @@ import {
UserEntity, UserEntity,
UserNotificationEntity, UserNotificationEntity,
UserOtpEntity, UserOtpEntity,
UserRoleEntity,
UserSpaceEntity, UserSpaceEntity,
} from './entities'; } from './entities';
@ -17,7 +16,6 @@ import {
UserEntity, UserEntity,
UserNotificationEntity, UserNotificationEntity,
UserOtpEntity, UserOtpEntity,
UserRoleEntity,
UserSpaceEntity, UserSpaceEntity,
]), ]),
], ],

View File

@ -7,7 +7,6 @@ import { GroupModule } from './group/group.module';
import { DeviceModule } from './device/device.module'; import { DeviceModule } from './device/device.module';
import { UserDevicePermissionModule } from './user-device-permission/user-device-permission.module'; import { UserDevicePermissionModule } from './user-device-permission/user-device-permission.module';
import { CommunityModule } from './community/community.module'; import { CommunityModule } from './community/community.module';
import { RoleModule } from './role/role.module';
import { SeederModule } from '@app/common/seed/seeder.module'; import { SeederModule } from '@app/common/seed/seeder.module';
import { UserNotificationModule } from './user-notification/user-notification.module'; import { UserNotificationModule } from './user-notification/user-notification.module';
import { DeviceMessagesSubscriptionModule } from './device-messages/device-messages.module'; import { DeviceMessagesSubscriptionModule } from './device-messages/device-messages.module';
@ -24,6 +23,8 @@ import { SpaceModule } from './space/space.module';
import { ProductModule } from './product'; import { ProductModule } from './product';
import { ProjectModule } from './project'; import { ProjectModule } from './project';
import { SpaceModelModule } from './space-model'; import { SpaceModelModule } from './space-model';
import { InviteUserModule } from './invite-user/invite-user.module';
import { PermissionModule } from './permission/permission.module';
@Module({ @Module({
imports: [ imports: [
ConfigModule.forRoot({ ConfigModule.forRoot({
@ -31,7 +32,7 @@ import { SpaceModelModule } from './space-model';
}), }),
AuthenticationModule, AuthenticationModule,
UserModule, UserModule,
RoleModule, InviteUserModule,
CommunityModule, CommunityModule,
SpaceModule, SpaceModule,
@ -51,6 +52,7 @@ import { SpaceModelModule } from './space-model';
ScheduleModule, ScheduleModule,
ProductModule, ProductModule,
ProjectModule, ProjectModule,
PermissionModule,
], ],
providers: [ providers: [
{ {

View File

@ -6,10 +6,7 @@ import { UserAuthController } from './controllers';
import { UserAuthService } from './services'; import { UserAuthService } from './services';
import { UserRepository } from '@app/common/modules/user/repositories'; import { UserRepository } from '@app/common/modules/user/repositories';
import { UserSessionRepository } from '@app/common/modules/session/repositories/session.repository'; import { UserSessionRepository } from '@app/common/modules/session/repositories/session.repository';
import { import { UserOtpRepository } from '@app/common/modules/user/repositories';
UserRoleRepository,
UserOtpRepository,
} from '@app/common/modules/user/repositories';
import { RoleTypeRepository } from '@app/common/modules/role-type/repositories'; import { RoleTypeRepository } from '@app/common/modules/role-type/repositories';
@Module({ @Module({
@ -20,7 +17,6 @@ import { RoleTypeRepository } from '@app/common/modules/role-type/repositories';
UserRepository, UserRepository,
UserSessionRepository, UserSessionRepository,
UserOtpRepository, UserOtpRepository,
UserRoleRepository,
RoleTypeRepository, RoleTypeRepository,
], ],
exports: [UserAuthService], exports: [UserAuthService],

View File

@ -134,13 +134,12 @@ export class UserAuthService {
isLoggedOut: false, isLoggedOut: false,
}), }),
]); ]);
const res = await this.authService.login({ const res = await this.authService.login({
email: user.email, email: user.email,
userId: user.uuid, userId: user.uuid,
uuid: user.uuid, uuid: user.uuid,
roles: user?.roles?.map((role) => { role: user.roleType,
return { uuid: role.uuid, type: role.roleType.type };
}),
sessionId: session[1].uuid, sessionId: session[1].uuid,
}); });
return res; return res;

View File

@ -0,0 +1,4 @@
import { SetMetadata } from '@nestjs/common';
export const Permissions = (...permissions: string[]) =>
SetMetadata('permissions', permissions);

View File

@ -0,0 +1,44 @@
import { Injectable, ExecutionContext } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Reflector } from '@nestjs/core';
import { RolePermissions } from '@app/common/constants/role-permissions';
import { RoleType } from '@app/common/constants/role.type.enum';
@Injectable()
export class PermissionsGuard extends AuthGuard('jwt') {
constructor(private reflector: Reflector) {
super();
}
async canActivate(context: ExecutionContext): Promise<boolean> {
// First, run the AuthGuard logic to validate the JWT
const isAuthenticated = await super.canActivate(context);
if (!isAuthenticated) {
return false;
}
// Authorization logic
const requiredPermissions = this.reflector.get<string[]>(
'permissions',
context.getHandler(),
);
if (!requiredPermissions) {
return true; // Allow if no permissions are specified
}
const request = context.switchToHttp().getRequest();
const user = request.user; // User is now available after AuthGuard
console.log('user', user);
const userRole = user?.role.type as RoleType;
if (!userRole || !RolePermissions[userRole]) {
return false; // Deny if role or permissions are missing
}
const userPermissions = RolePermissions[userRole];
// Check if the user has the required permissions
return requiredPermissions.every((perm) => userPermissions.includes(perm));
}
}

View File

@ -0,0 +1 @@
export * from './invite-user.controller';

View File

@ -0,0 +1,34 @@
import { InviteUserService } from '../services/invite-user.service';
import { Body, Controller, Post, UseGuards } from '@nestjs/common';
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
import { AddUserInvitationDto } from '../dtos/add.invite-user.dto';
import { ControllerRoute } from '@app/common/constants/controller-route';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { PermissionsGuard } from 'src/guards/permissions.guard';
import { Permissions } from 'src/decorators/permissions.decorator';
@ApiTags('Invite User Module')
@Controller({
version: '1',
path: ControllerRoute.INVITE_USER.ROUTE,
})
export class InviteUserController {
constructor(private readonly inviteUserService: InviteUserService) {}
@ApiBearerAuth()
@UseGuards(PermissionsGuard)
@Permissions('USER_ADD')
@Post()
@ApiOperation({
summary: ControllerRoute.INVITE_USER.ACTIONS.CREATE_USER_INVITATION_SUMMARY,
description:
ControllerRoute.INVITE_USER.ACTIONS.CREATE_USER_INVITATION_DESCRIPTION,
})
async createUserInvitation(
@Body() addUserInvitationDto: AddUserInvitationDto,
): Promise<BaseResponseDto> {
return await this.inviteUserService.createUserInvitation(
addUserInvitationDto,
);
}
}

View File

@ -0,0 +1,75 @@
import { ApiProperty } from '@nestjs/swagger';
import {
ArrayMinSize,
IsArray,
IsNotEmpty,
IsOptional,
IsString,
} from 'class-validator';
export class AddUserInvitationDto {
@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 email of the user',
example: 'OqM9A@example.com',
required: true,
})
@IsString()
@IsNotEmpty()
public email: string;
@ApiProperty({
description: 'The job title of the user',
example: 'Software Engineer',
required: true,
})
@IsString()
@IsNotEmpty()
public jobTitle: string;
@ApiProperty({
description: 'The phone number of the user',
example: '+1234567890',
required: true,
})
@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 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<AddUserInvitationDto>) {
Object.assign(this, dto);
}
}

View File

@ -0,0 +1 @@
export * from './add.invite-user.dto';

View File

@ -0,0 +1,23 @@
import { Module } from '@nestjs/common';
import { InviteUserService } from './services/invite-user.service';
import { InviteUserController } from './controllers/invite-user.controller';
import { ConfigModule } from '@nestjs/config';
import {
InviteUserRepository,
InviteUserSpaceRepository,
} from '@app/common/modules/Invite-user/repositories';
import { UserRepository } from '@app/common/modules/user/repositories';
import { InviteUserRepositoryModule } from '@app/common/modules/Invite-user/Invite-user.repository.module';
@Module({
imports: [ConfigModule, InviteUserRepositoryModule],
controllers: [InviteUserController],
providers: [
InviteUserService,
InviteUserRepository,
UserRepository,
InviteUserSpaceRepository,
],
exports: [InviteUserService],
})
export class InviteUserModule {}

View File

@ -0,0 +1 @@
export * from './invite-user.service';

View File

@ -0,0 +1 @@
export * from './permission.controller';

View File

@ -0,0 +1,24 @@
import { Controller, Get, Param } from '@nestjs/common';
import { ApiTags, ApiOperation } from '@nestjs/swagger';
import { ControllerRoute } from '@app/common/constants/controller-route';
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
import { PermissionService } from '../services';
@ApiTags('Permission Module')
@Controller({
version: EnableDisableStatusEnum.ENABLED,
path: ControllerRoute.PERMISSION.ROUTE,
})
export class PermissionController {
constructor(private readonly permissionService: PermissionService) {}
@Get(':roleUuid')
@ApiOperation({
summary: ControllerRoute.PERMISSION.ACTIONS.GET_PERMISSION_BY_ROLE_SUMMARY,
description:
ControllerRoute.PERMISSION.ACTIONS.GET_PERMISSION_BY_ROLE_DESCRIPTION,
})
async getPermissionsByRole(@Param('roleUuid') roleUuid: string) {
return await this.permissionService.getPermissionsByRole(roleUuid);
}
}

View File

@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { CommonModule } from '@app/common';
import { PermissionController } from './controllers';
import { PermissionService } from './services';
import { RoleTypeRepository } from '@app/common/modules/role-type/repositories';
@Module({
imports: [ConfigModule, CommonModule],
controllers: [PermissionController],
providers: [PermissionService, RoleTypeRepository],
exports: [PermissionService],
})
export class PermissionModule {}

View File

@ -0,0 +1 @@
export * from './permission.service';

View File

@ -0,0 +1,52 @@
import { PermissionMapping } from '@app/common/constants/permissions-mapping';
import { RolePermissions } from '@app/common/constants/role-permissions';
import { RoleType } from '@app/common/constants/role.type.enum';
import { RoleTypeRepository } from '@app/common/modules/role-type/repositories';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
@Injectable()
export class PermissionService {
constructor(private readonly roleTypeRepository: RoleTypeRepository) {}
async getPermissionsByRole(roleUuid: string) {
try {
const role = await this.roleTypeRepository.findOne({
where: {
uuid: roleUuid,
},
});
if (!role) {
throw new HttpException('Role not found', HttpStatus.NOT_FOUND);
}
const permissions = this.mapPermissions(role.type.toString() as RoleType);
return permissions;
} catch (err) {
throw new HttpException(
err.message || 'Internal Server Error',
err.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
mapPermissions(role: RoleType): any[] {
const rolePermissions = RolePermissions[role]; // Permissions for the role
const mappedPermissions = Object.entries(PermissionMapping).map(
([title, subOptions]) => ({
title,
subOptions: Object.entries(subOptions).map(
([subTitle, permissions]) => ({
title: subTitle,
subOptions: permissions.map((permission) => ({
title: permission,
isChecked: rolePermissions.includes(`${subTitle}_${permission}`), // Check if the role has the permission
})),
}),
),
}),
);
return mappedPermissions;
}
}