Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: change users role and permissions #495

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions backend/migrations/1733147745970-update-role-and-permissions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class UpdateRoleAndPermissions1733147745970
implements MigrationInterface
{
name = 'UpdateRoleAndPermissions1733147745970';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "users" DROP CONSTRAINT "FK_a2cecd1a3531c0b041e29ba46e1"`,
);
await queryRunner.query(
`ALTER TYPE "public"."permissions_code_enum" RENAME TO "permissions_code_enum_old"`,
);
await queryRunner.query(
`CREATE TYPE "public"."permissions_code_enum" AS ENUM('manage_cc_members', 'add_constitution_version', 'manage_admins', 'manage_roles_and_permissions')`,
);
await queryRunner.query(
`ALTER TABLE "permissions" ALTER COLUMN "code" TYPE "public"."permissions_code_enum" USING "code"::"text"::"public"."permissions_code_enum"`,
);
await queryRunner.query(`DROP TYPE "public"."permissions_code_enum_old"`);
await queryRunner.query(
`ALTER TABLE "users" ADD CONSTRAINT "FK_a2cecd1a3531c0b041e29ba46e1" FOREIGN KEY ("role_id") REFERENCES "roles"("id") ON DELETE NO ACTION ON UPDATE CASCADE`,
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "users" DROP CONSTRAINT "FK_a2cecd1a3531c0b041e29ba46e1"`,
);
await queryRunner.query(
`CREATE TYPE "public"."permissions_code_enum_old" AS ENUM('manage_cc_members', 'add_constitution_version', 'manage_admins')`,
);
await queryRunner.query(
`ALTER TABLE "permissions" ALTER COLUMN "code" TYPE "public"."permissions_code_enum_old" USING "code"::"text"::"public"."permissions_code_enum_old"`,
);
await queryRunner.query(`DROP TYPE "public"."permissions_code_enum"`);
await queryRunner.query(
`ALTER TYPE "public"."permissions_code_enum_old" RENAME TO "permissions_code_enum"`,
);
await queryRunner.query(
`ALTER TABLE "users" ADD CONSTRAINT "FK_a2cecd1a3531c0b041e29ba46e1" FOREIGN KEY ("role_id") REFERENCES "roles"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
}
}
50 changes: 50 additions & 0 deletions backend/migrations/1733148028374-add-manage-permissions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddManagePermissions1733148028374 implements MigrationInterface {
name = 'AddManagePermissions1733148028374';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
-- Add permission 'manage_roles_and_permissions'
INSERT INTO permissions ("id", "code", "created_at") VALUES
(uuid_generate_v4(), 'manage_roles_and_permissions', NOW());
`);

await queryRunner.query(`
-- Add permission 'manage_roles_and_permissions' to the super_admin role
INSERT INTO role_permissions(role_id, permission_id)
SELECT roles.id, permissions.id
FROM roles
INNER JOIN permissions ON permissions.code = 'manage_roles_and_permissions'
WHERE roles.code = 'super_admin';
`);

await queryRunner.query(`
-- Add permission 'manage_roles_and_permissions' to registered super_admin
INSERT INTO user_permissions(user_id, permission_id)
SELECT users.id, permissions.id
FROM permissions
INNER join users on users.role_id = (SELECT id from roles where code = 'super_admin')
WHERE permissions.code = 'manage_roles_and_permissions';
`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
-- Remove permission 'manage_roles_and_permissions' from registered super_admin
DELETE FROM user_permissions
WHERE permission_id = (SELECT id FROM permissions WHERE code = 'manage_roles_and_permissions');
`);

await queryRunner.query(`
-- Remove permission 'manage_roles_and_permissions' from the super_admin role
DELETE FROM role_permissions
WHERE permission_id = (SELECT id FROM permissions WHERE code = 'manage_roles_and_permissions');
`);

await queryRunner.query(`
-- Remove permission 'manage_roles_and_permissions'
DELETE FROM permissions WHERE code = 'manage_roles_and_permissions';
`);
}
}
37 changes: 36 additions & 1 deletion backend/postman/CC Portal develop.postman_collection.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"info": {
"_postman_id": "76b45677-bce1-4b13-8de8-db1a76d8e827",
"_postman_id": "2b1baa78-48e2-4671-aff8-da723a452325",
"name": "CC Portal develop",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
"_exporter_id": "20133713"
Expand Down Expand Up @@ -278,6 +278,41 @@
}
},
"response": []
},
{
"name": "Change user role and permissions",
"request": {
"method": "PATCH",
"header": [
{
"key": "Authorization",
"value": "{{accessToken}}",
"type": "text"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"user_id\": \"1c3b9794-95c1-4c78-9948-b057996763f6\",\n \"new_role\": \"admin\",\n \"new_permissions\": [\"manage_cc_members\"]\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{base-url}}/api/users/{{userId}}/role-permissions",
"host": [
"{{base-url}}"
],
"path": [
"api",
"users",
"{{userId}}",
"role-permissions"
]
}
},
"response": []
}
]
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { RoleEnum } from 'src/users/enums/role.enum';
import { PermissionEnum } from 'src/users/enums/permission.enum';
import { ApiProperty } from '@nestjs/swagger';
import { ArrayUnique, IsArray, IsEnum, IsIn, IsUUID } from 'class-validator';

export class UpdateRoleAndPermissionsRequest {
@ApiProperty({
description: 'Identification number of the user',
example: '7ceb9ab7-6427-40b7-be2e-37ba6742d5fd',
name: 'user_id',
})
@IsUUID()
userId: string;

@ApiProperty({
description: 'New role of the user',
example: 'admin',
name: 'new_role',
})
@IsIn([RoleEnum.USER, RoleEnum.ADMIN])
newRole: string;

@ApiProperty({
description: 'New list of permissions for the user (optional)',
example: ['manage_cc_members'],
required: false,
name: 'new_permissions',
})
@IsArray()
@IsEnum(PermissionEnum, { each: true })
@ArrayUnique({ message: 'Permissions must be unique' })
newPermissions?: string[] = [];
}
35 changes: 35 additions & 0 deletions backend/src/users/api/users.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { ToggleStatusRequest } from './request/toggle-status.request';
import { ApiConditionalExcludeEndpoint } from 'src/common/decorators/api-conditional-exclude-endpoint.decorator';
import { Permissions } from 'src/auth/guard/permission.decorator';
import { RemoveUserRequest } from './request/remove-user.request';
import { UpdateRoleAndPermissionsRequest } from './request/update-role-and-permissions.request';

@ApiTags('Users')
@Controller('users')
Expand Down Expand Up @@ -307,4 +308,38 @@ export class UsersController {
message: 'User deleted successfully',
};
}

@ApiConditionalExcludeEndpoint()
@ApiBearerAuth('JWT-auth')
@ApiOperation({
summary: 'Update user role and permissions by superadmin',
})
@ApiParam({
name: 'id',
required: true,
description: 'Identification number of the user',
type: String,
})
@ApiBody({ type: UpdateRoleAndPermissionsRequest })
@ApiResponse({
status: 200,
description: 'User updated successfully.',
type: UserResponse,
})
@ApiResponse({ status: 400, description: 'Bad request' })
@ApiResponse({ status: 403, description: 'Forbidden resource' })
@ApiResponse({ status: 404, description: 'Not Found' })
@ApiResponse({ status: 500, description: 'Internal server error' })
@HttpCode(200)
@Patch(':id/role-permissions')
@Permissions(PermissionEnum.MANAGE_ROLES_AND_PERMISSIONS)
@UseGuards(JwtAuthGuard, UserPathGuard, PermissionGuard)
async updateUserRoleAndPermissions(
@Param('id', ParseUUIDPipe) id: string,
@Body() updateRoleAndPermissionsRequest: UpdateRoleAndPermissionsRequest,
): Promise<UserResponse> {
return await this.usersFacade.updateUserRoleAndPermissions(
updateRoleAndPermissionsRequest,
);
}
}
2 changes: 2 additions & 0 deletions backend/src/users/entities/user.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,14 @@ export class User extends CommonEntity {
@Index('users_role_id_idx')
@ManyToOne(() => Role, (role) => role.users, {
eager: true,
onUpdate: 'CASCADE',
})
@JoinColumn({ name: 'role_id' })
role: Role;

@ManyToMany(() => Permission, (permission) => permission.users, {
eager: true,
onUpdate: 'CASCADE',
})
@JoinTable({
name: 'user_permissions',
Expand Down
1 change: 1 addition & 0 deletions backend/src/users/enums/permission.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export enum PermissionEnum {
MANAGE_CC_MEMBERS = 'manage_cc_members',
ADD_CONSTITUTION = 'add_constitution_version',
MANAGE_ADMINS = 'manage_admins',
MANAGE_ROLES_AND_PERMISSIONS = 'manage_roles_and_permissions',
}
10 changes: 10 additions & 0 deletions backend/src/users/facade/users.facade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { PaginationDtoMapper } from 'src/util/pagination/mapper/pagination.mappe
import { PermissionEnum } from '../enums/permission.enum';
import { ToggleStatusRequest } from '../api/request/toggle-status.request';
import { UserStatusEnum } from '../enums/user-status.enum';
import { UpdateRoleAndPermissionsRequest } from '../api/request/update-role-and-permissions.request';
@Injectable()
export class UsersFacade {
private logger = new Logger(UsersService.name);
Expand Down Expand Up @@ -131,4 +132,13 @@ export class UsersFacade {
);
}
}

async updateUserRoleAndPermissions(
updateRoleAndPermissionsRequest: UpdateRoleAndPermissionsRequest,
): Promise<UserResponse> {
const user = await this.usersService.updateUserRoleAndPermissions(
updateRoleAndPermissionsRequest,
);
return UserMapper.mapUserDtoToResponse(user);
}
}
45 changes: 45 additions & 0 deletions backend/src/users/services/users.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { PaginationEntityMapper } from 'src/util/pagination/mapper/pagination.ma
import { Paginator } from 'src/util/pagination/paginator';
import { RoleFactory } from '../role/role.factory';
import { PermissionEnum } from '../enums/permission.enum';
import { UpdateRoleAndPermissionsRequest } from '../api/request/update-role-and-permissions.request';

@Injectable()
export class UsersService {
Expand Down Expand Up @@ -292,4 +293,48 @@ export class UsersService {
const user = await this.findEntityById(userId);
await this.userRepository.remove(user);
}

async updateUserRoleAndPermissions(
updateRoleAndPermissionsRequest: UpdateRoleAndPermissionsRequest,
): Promise<UserDto> {
const user = await this.findEntityById(
updateRoleAndPermissionsRequest.userId,
);
if (user.role.code === RoleEnum.SUPER_ADMIN) {
throw new ForbiddenException(`You have no permission for this action`);
}
const role = await this.findRoleByCode(
updateRoleAndPermissionsRequest.newRole,
);
this.validatePermissionsForRole(
role,
updateRoleAndPermissionsRequest.newPermissions,
);

user.role = role;
if (updateRoleAndPermissionsRequest.newPermissions) {
const newPermissions = await this.getUserPermissions(
updateRoleAndPermissionsRequest.newPermissions,
);
user.permissions = newPermissions;
}
await this.userRepository.save(user);

return UserMapper.userToDto(user);
}

private validatePermissionsForRole(role: Role, permissions: string[]): void {
if (role.code === 'admin' && permissions.length === 0) {
throw new BadRequestException(`At least one permission is required`);
}
const allowedPermissions = role.permissions?.map(
(permission) => permission.code,
);
const isAllowed = permissions.every((perm) =>
allowedPermissions.includes(perm),
);
if (!isAllowed) {
throw new BadRequestException(`Permissions aren't allowed for this role`);
}
}
}
Loading