From c925d2e5398a53f7b1fa836b682bab601c38ce5a Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Wed, 22 Jul 2026 12:19:37 +0330 Subject: [PATCH] multi role --- src/modules/auth/providers/sso.service.ts | 5 ++- src/modules/auth/providers/tokens.service.ts | 3 +- src/modules/users/DTO/create-admin.dto.ts | 10 +++-- src/modules/users/providers/admins.service.ts | 38 +++++++++---------- .../users/providers/dsc.code-workspace | 11 ++++++ .../users/utils/role-permissions.util.ts | 19 ++++++++++ 6 files changed, 60 insertions(+), 26 deletions(-) create mode 100644 src/modules/users/providers/dsc.code-workspace create mode 100644 src/modules/users/utils/role-permissions.util.ts diff --git a/src/modules/auth/providers/sso.service.ts b/src/modules/auth/providers/sso.service.ts index 906160a..39680b2 100644 --- a/src/modules/auth/providers/sso.service.ts +++ b/src/modules/auth/providers/sso.service.ts @@ -7,6 +7,7 @@ import { SSOClientService } from "./sso-client.service"; import { TokensService } from "./tokens.service"; import { AuthMessage } from "../../../common/enums/message.enum"; import { User } from "../../users/entities/user.entity"; +import { getPermissionsFromRoles } from "../../users/utils/role-permissions.util"; import { ITokenPayload } from "../interfaces/IToken-payload"; @Injectable() @@ -46,7 +47,7 @@ export class SSOService { const payload: ITokenPayload = { id: user.id, isAdmin: user.roles.some((r) => r.isAdmin), - permissions: user.roles.flatMap((r) => (r?.permissions?.length ? r.permissions.map((p) => p.name) : [])), + permissions: getPermissionsFromRoles(user.roles), audience: clientId, }; @@ -105,7 +106,7 @@ export class SSOService { const payload: ITokenPayload = { id: user.id, isAdmin: user.roles.some((r) => r.isAdmin), - permissions: user.roles.flatMap((r) => (r?.permissions?.length ? r.permissions.map((p) => p.name) : [])), + permissions: getPermissionsFromRoles(user.roles), audience: clientId, }; diff --git a/src/modules/auth/providers/tokens.service.ts b/src/modules/auth/providers/tokens.service.ts index 8ac7a26..064bb5d 100755 --- a/src/modules/auth/providers/tokens.service.ts +++ b/src/modules/auth/providers/tokens.service.ts @@ -9,6 +9,7 @@ import { DataSource, QueryRunner } from "typeorm"; import { AuthMessage } from "../../../common/enums/message.enum"; import { User } from "../../users/entities/user.entity"; import { RefreshTokensRepository } from "../../users/repositories/refresh-token.repository"; +import { getPermissionsFromRoles } from "../../users/utils/role-permissions.util"; import { ITokenPayload } from "../interfaces/IToken-payload"; import { ResellerType } from "../DTO/verify-reseller-otp.dto"; @@ -28,7 +29,7 @@ export class TokensService { { id: user.id, isAdmin: user.roles.some((r) => r.isAdmin), - permissions: user.roles.flatMap((r) => (r?.permissions?.length ? r.permissions.map((p) => p.name) : [])), + permissions: getPermissionsFromRoles(user.roles), }, queryRunner, ); diff --git a/src/modules/users/DTO/create-admin.dto.ts b/src/modules/users/DTO/create-admin.dto.ts index 77768a5..8bfade8 100755 --- a/src/modules/users/DTO/create-admin.dto.ts +++ b/src/modules/users/DTO/create-admin.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; -import { IsEmail, IsMobilePhone, IsNotEmpty, IsOptional, IsString, IsUUID, IsUrl, Length, MinLength } from "class-validator"; +import { ArrayNotEmpty, IsArray, IsEmail, IsMobilePhone, IsNotEmpty, IsOptional, IsString, IsUUID, IsUrl, Length, MinLength } from "class-validator"; import { AdminMessage, AuthMessage } from "../../../common/enums/message.enum"; @@ -22,9 +22,11 @@ export class CreateAdminDto { phone: string; @IsNotEmpty({ message: AdminMessage.ROLE_REQUIRED }) - @IsUUID("4", { message: AdminMessage.ROLE_ID_SHOULD_BE_UUID }) - @ApiProperty({ description: "Role id", example: "123e4567-e89b-12d3-a456-426614174000" }) - roleId: string; + @IsArray({ message: AdminMessage.ROLE_REQUIRED }) + @ArrayNotEmpty({ message: AdminMessage.ROLE_REQUIRED }) + @IsUUID("4", { each: true, message: AdminMessage.ROLE_ID_SHOULD_BE_UUID }) + @ApiProperty({ description: "Role ids", example: ["123e4567-e89b-12d3-a456-426614174000"], type: [String] }) + roleIds: string[]; @IsNotEmpty({ message: AuthMessage.FIRST_NAME_NOT_EMPTY }) @IsString({ message: AuthMessage.FIRST_NAME_NOT_EMPTY }) diff --git a/src/modules/users/providers/admins.service.ts b/src/modules/users/providers/admins.service.ts index 83dfaa8..afd5a85 100755 --- a/src/modules/users/providers/admins.service.ts +++ b/src/modules/users/providers/admins.service.ts @@ -18,6 +18,7 @@ import { RoleEnum } from "../enums/role.enum"; import { PermissionsRepository } from "../repositories/permissions.repository"; import { RoleRepository } from "../repositories/roles.repository"; import { UserRepository } from "../repositories/users.repository"; +import { getPermissionEntitiesFromRoles } from "../utils/role-permissions.util"; @Injectable() export class AdminsService { @@ -43,22 +44,22 @@ export class AdminsService { const existPhone = await queryRunner.manager.findOneBy(this.userRepository.target, { phone: createDto.phone }); if (existPhone) throw new BadRequestException(UserMessage.PHONE_EXIST); - const role = await queryRunner.manager.findOne(this.rolesRepository.target, { - where: { id: createDto.roleId }, + const roles = await queryRunner.manager.find(this.rolesRepository.target, { + where: { id: In(createDto.roleIds), isAdmin: true }, relations: { permissions: true }, }); - if (!role) throw new BadRequestException(AdsMessage.ROLE_NOT_FOUND); + if (roles.length !== createDto.roleIds.length) throw new BadRequestException(AdsMessage.ROLE_NOT_FOUND); if (createDto.password !== createDto.repeatPassword) throw new BadRequestException(AdminMessage.PASSWORD_NOT_MATCH); const hashedPassword = await this.passwordService.hashPassword(createDto.password); const userName = slugify(`${createDto.firstName} ${Date.now().toString().slice(-5)}`, { lower: true, trim: true }); - await queryRunner.manager.save(this.rolesRepository.target, role); + const { roleIds: _roleIds, repeatPassword: _repeatPassword, ...adminData } = createDto; const adminUser = queryRunner.manager.create(this.userRepository.target, { - ...createDto, - roles: [role], + ...adminData, + roles, password: hashedPassword, userName, }); @@ -75,7 +76,7 @@ export class AdminsService { userAgent: userIpAndHeaders.headers["user-agent"], actionDescription: `Admin created new admin: ${createDto.email}`, metadata: { - roleId: createDto.roleId, + roleIds: createDto.roleIds, createdBy: "admin", email: createDto.email, phone: createDto.phone, @@ -118,16 +119,14 @@ export class AdminsService { if (existPhone) throw new BadRequestException(UserMessage.PHONE_EXIST); } - if (updateDto.roleId) { - const role = await queryRunner.manager.findOne(this.rolesRepository.target, { - where: { id: updateDto.roleId }, + if (updateDto.roleIds?.length) { + const roles = await queryRunner.manager.find(this.rolesRepository.target, { + where: { id: In(updateDto.roleIds), isAdmin: true }, relations: { permissions: true }, }); - if (!role) throw new BadRequestException(AdsMessage.ROLE_NOT_FOUND); + if (roles.length !== updateDto.roleIds.length) throw new BadRequestException(AdsMessage.ROLE_NOT_FOUND); - await queryRunner.manager.save(this.rolesRepository.target, role); - - admin.roles = [role]; + admin.roles = roles; await queryRunner.manager.save(this.userRepository.target, admin); } @@ -138,7 +137,8 @@ export class AdminsService { admin.password = await this.passwordService.hashPassword(updateDto.password); } - await queryRunner.manager.save(this.userRepository.target, { ...admin, ...updateDto, password: admin.password }); + const { roleIds: _roleIds, repeatPassword: _repeatPassword, ...updateData } = updateDto; + await queryRunner.manager.save(this.userRepository.target, { ...admin, ...updateData, password: admin.password }); await queryRunner.commitTransaction(); await this.accessLogService.logUpdate("User", admin.id, UserType.ADMIN, { @@ -150,7 +150,7 @@ export class AdminsService { userType: UserType.ADMIN, actionDescription: `Admin updated admin: ${admin.email}`, metadata: { - roleId: updateDto.roleId, + roleIds: updateDto.roleIds, }, oldValues: JSON.stringify({ ...admin }), newValues: JSON.stringify({ ...updateDto }), @@ -169,7 +169,7 @@ export class AdminsService { /************************************************************ */ async deleteAdmin(id: string, userIpAndHeaders: IUserIpAndHeaders) { - const admin = await this.userRepository.findOne({ where: { id, roles: { isAdmin: true } } }); + const admin = await this.userRepository.findOne({ where: { id, roles: { isAdmin: true } }, relations: { roles: true } }); if (!admin) throw new BadRequestException(AdminMessage.ADMIN_NOT_FOUND); await this.userRepository.softDelete(id); @@ -182,7 +182,7 @@ export class AdminsService { userAgent: userIpAndHeaders.headers["user-agent"], actionDescription: `Admin deleted admin: ${admin.email}`, metadata: { - roleId: admin.roles[0].id, + roleIds: admin.roles.map((role) => role.id), email: admin.email, phone: admin.phone, }, @@ -303,7 +303,7 @@ export class AdminsService { if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND); return { - permissions: user.roles[0].permissions, + permissions: getPermissionEntitiesFromRoles(user.roles), }; } /************************************************************ */ diff --git a/src/modules/users/providers/dsc.code-workspace b/src/modules/users/providers/dsc.code-workspace new file mode 100644 index 0000000..fea94a0 --- /dev/null +++ b/src/modules/users/providers/dsc.code-workspace @@ -0,0 +1,11 @@ +{ + "folders": [ + { + "path": "../../../.." + }, + { + "path": "../../../../../danak-admin" + } + ], + "settings": {} +} \ No newline at end of file diff --git a/src/modules/users/utils/role-permissions.util.ts b/src/modules/users/utils/role-permissions.util.ts new file mode 100644 index 0000000..ae39ef1 --- /dev/null +++ b/src/modules/users/utils/role-permissions.util.ts @@ -0,0 +1,19 @@ +import { PermissionEnum } from "../enums/permission.enum"; +import { Role } from "../entities/role.entity"; +import { Permission } from "../entities/permission.entity"; + +export function getPermissionsFromRoles(roles: Role[]): PermissionEnum[] { + return [...new Set(roles.flatMap((role) => (role?.permissions?.length ? role.permissions.map((permission) => permission.name) : [])))]; +} + +export function getPermissionEntitiesFromRoles(roles: Role[]): Permission[] { + const permissionsById = new Map(); + + for (const role of roles) { + for (const permission of role?.permissions ?? []) { + permissionsById.set(permission.id, permission); + } + } + + return [...permissionsById.values()]; +}