diff --git a/database/seeder.runner.ts b/database/seeder.runner.ts index bb3bceb..0fb8930 100644 --- a/database/seeder.runner.ts +++ b/database/seeder.runner.ts @@ -1,11 +1,11 @@ import { Logger } from "@nestjs/common"; import { connectionSource } from "./connection"; -// import { seedAdmin } from "./seeders/admin.seeder"; +import { seedAdmin } from "./seeders/admin.seeder"; import { seedCityAndProvince } from "./seeders/iran-city.seeder"; -// import { seedNotifSettings } from "./seeders/notif-setting.seeder"; -// import { seedPaymentGateways } from "./seeders/payment-gateway.seeder"; -// import { seedRole } from "./seeders/role.seeder"; +import { seedNotifSettings } from "./seeders/notif-setting.seeder"; +import { seedPaymentGateways } from "./seeders/payment-gateway.seeder"; +import { seedPermissionsAndRoles } from "./seeders/role.seeder"; const logger = new Logger("Seeder"); @@ -13,11 +13,11 @@ export const runSeeder = async () => { await connectionSource.initialize(); logger.log("start seeding database"); // - // await seedRole(connectionSource, logger); - // await seedAdmin(connectionSource, logger); - // await seedNotifSettings(connectionSource, logger); - // await seedPaymentGateways(connectionSource, logger); await seedCityAndProvince(connectionSource, logger); + await seedPermissionsAndRoles(connectionSource, logger); + await seedAdmin(connectionSource, logger); + await seedNotifSettings(connectionSource, logger); + await seedPaymentGateways(connectionSource, logger); logger.log("seeding completed"); process.exit(0); diff --git a/database/seeders/admin.seeder.ts b/database/seeders/admin.seeder.ts index 1fc2b34..0da370f 100644 --- a/database/seeders/admin.seeder.ts +++ b/database/seeders/admin.seeder.ts @@ -26,7 +26,7 @@ export const seedAdmin = async (dataSource: DataSource, logger: Logger) => { const hashedPassword = await hash(defaultAdmin.password!, 10); - const admin = userRepo.create({ ...defaultAdmin, password: hashedPassword, role: adminRole }); + const admin = userRepo.create({ ...defaultAdmin, password: hashedPassword, roles: [adminRole] }); await userRepo.save(admin); logger.log("admin created successfully"); } catch (error) { diff --git a/database/seeders/role.seeder.ts b/database/seeders/role.seeder.ts index 2e86b4e..2a47dc0 100644 --- a/database/seeders/role.seeder.ts +++ b/database/seeders/role.seeder.ts @@ -1,21 +1,68 @@ import { Logger } from "@nestjs/common"; import { DataSource } from "typeorm"; +import { Permission } from "../../src/modules/users/entities/permission.entity"; import { Role } from "../../src/modules/users/entities/role.entity"; +import { PermissionEnum } from "../../src/modules/users/enums/permission.enum"; import { RoleEnum } from "../../src/modules/users/enums/role.enum"; -const roles = [{ name: RoleEnum.SUPER_ADMIN }, { name: RoleEnum.ADMIN }, { name: RoleEnum.VISITOR }, { name: RoleEnum.USER }]; +// Define permissions +const permissions = Object.values(PermissionEnum).map((perm) => ({ name: perm })); -export const seedRole = async (dataSource: DataSource, logger: Logger) => { +// Define roles with their default permissions +const rolesWithPermissions: { name: RoleEnum; permissions: PermissionEnum[] }[] = [ + { + name: RoleEnum.ADMIN, + permissions: Object.values(PermissionEnum), + }, + { + name: RoleEnum.AGENT, + permissions: [PermissionEnum.CUSTOMERS, PermissionEnum.TICKETS, PermissionEnum.TRANSACTIONS], + }, + { + name: RoleEnum.DEVELOPER, + permissions: [PermissionEnum.SERVICES, PermissionEnum.LOGS, PermissionEnum.LEARNINGS], + }, + { + name: RoleEnum.USER, + permissions: [], + }, +]; + +export const seedPermissionsAndRoles = async (dataSource: DataSource, logger: Logger) => { try { + const permissionRepo = dataSource.getRepository(Permission); const roleRepo = dataSource.getRepository(Role); - for (const role of roles) { - const newRole = roleRepo.create(role); - await roleRepo.save(newRole); + + // Seed Permissions + logger.log("Seeding Permissions..."); + for (const perm of permissions) { + const existingPerm = await permissionRepo.findOne({ where: { name: perm.name } }); + if (!existingPerm) { + const newPerm = permissionRepo.create(perm); + await permissionRepo.save(newPerm); + } } - logger.log("Role seeded successfully"); + logger.log("Permissions seeded successfully"); + + // Fetch all saved permissions + const allPermissions = await permissionRepo.find(); + + // Seed Roles with permissions + logger.log("Seeding Roles with Permissions..."); + for (const roleData of rolesWithPermissions) { + let role = await roleRepo.findOne({ where: { name: roleData.name }, relations: ["permissions"] }); + + if (!role) { + role = roleRepo.create({ name: roleData.name, permissions: [] }); + } + + role.permissions = allPermissions.filter((perm) => roleData.permissions.includes(perm.name as PermissionEnum)); + await roleRepo.save(role); + } + logger.log("Roles and Permissions seeded successfully"); } catch (error) { - logger.error("Error in seeding role", error); + logger.error("Error in seeding roles and permissions", error); process.exit(1); } }; diff --git a/src/common/decorators/auth-guard.decorator.ts b/src/common/decorators/auth-guard.decorator.ts index 19bf11b..2941ba0 100644 --- a/src/common/decorators/auth-guard.decorator.ts +++ b/src/common/decorators/auth-guard.decorator.ts @@ -2,8 +2,9 @@ import { UseGuards, applyDecorators } from "@nestjs/common"; import { ApiBearerAuth } from "@nestjs/swagger"; import { JwtAuthGuard } from "../../modules/auth/guards/auth.guard"; +import { PermissionsGuard } from "../../modules/auth/guards/permission.guard"; import { RoleGuard } from "../../modules/auth/guards/role.guard"; export function AuthGuards() { - return applyDecorators(UseGuards(JwtAuthGuard, RoleGuard), ApiBearerAuth("authorization")); + return applyDecorators(UseGuards(JwtAuthGuard, RoleGuard, PermissionsGuard), ApiBearerAuth("authorization")); } diff --git a/src/common/decorators/permission.decorator.ts b/src/common/decorators/permission.decorator.ts new file mode 100644 index 0000000..ed71dee --- /dev/null +++ b/src/common/decorators/permission.decorator.ts @@ -0,0 +1,6 @@ +import { SetMetadata } from "@nestjs/common"; + +import { PermissionEnum } from "../../modules/users/enums/permission.enum"; + +export const PERMISSION_KEY = "permissions"; +export const PermissionsDec = (...permissions: PermissionEnum[]) => SetMetadata(PERMISSION_KEY, permissions); diff --git a/src/common/decorators/user.decorator.ts b/src/common/decorators/user.decorator.ts index ae0189e..2053278 100644 --- a/src/common/decorators/user.decorator.ts +++ b/src/common/decorators/user.decorator.ts @@ -1,15 +1,16 @@ import { ExecutionContext, createParamDecorator } from "@nestjs/common"; import { FastifyRequest } from "fastify"; -import { User } from "../../modules/users/entities/user.entity"; +import { ITokenPayload } from "../../modules/auth/interfaces/IToken-payload"; declare module "fastify" { interface FastifyRequest { - user?: Omit; + // user?: Omit; + user?: ITokenPayload; } } -export const UserDec = createParamDecorator((data: keyof Omit | undefined, ctx: ExecutionContext) => { +export const UserDec = createParamDecorator((data: keyof ITokenPayload | undefined, ctx: ExecutionContext) => { const req = ctx.switchToHttp().getRequest(); const user = req.user; diff --git a/src/modules/announcements/announcement.controller.ts b/src/modules/announcements/announcement.controller.ts index b3dcb69..3c67ca2 100644 --- a/src/modules/announcements/announcement.controller.ts +++ b/src/modules/announcements/announcement.controller.ts @@ -37,7 +37,7 @@ export class AnnouncementController { } @AuthGuards() - @Roles(RoleEnum.ADMIN, RoleEnum.USER, RoleEnum.VISITOR) + @Roles(RoleEnum.ADMIN, RoleEnum.USER) @ApiProperty({ description: "Get all announcements ===> login as user", }) @@ -47,7 +47,7 @@ export class AnnouncementController { } @AuthGuards() - @Roles(RoleEnum.ADMIN, RoleEnum.USER, RoleEnum.VISITOR) + @Roles(RoleEnum.ADMIN, RoleEnum.USER) @ApiProperty({ description: "Get one announcements with id", }) diff --git a/src/modules/auth/guards/permission.guard.ts b/src/modules/auth/guards/permission.guard.ts new file mode 100644 index 0000000..5dbf371 --- /dev/null +++ b/src/modules/auth/guards/permission.guard.ts @@ -0,0 +1,32 @@ +import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from "@nestjs/common"; +import { Reflector } from "@nestjs/core"; +import { FastifyRequest } from "fastify"; + +import { PERMISSION_KEY } from "../../../common/decorators/permission.decorator"; +import { AuthMessage } from "../../../common/enums/message.enum"; +import { PermissionEnum } from "../../users/enums/permission.enum"; + +@Injectable() +export class PermissionsGuard implements CanActivate { + constructor(private reflector: Reflector) {} + + canActivate(context: ExecutionContext) { + const requiredPermissions = this.reflector.getAllAndOverride(PERMISSION_KEY, [ + context.getHandler(), + context.getClass(), + ]); + + if (!requiredPermissions) return true; + + const request = context.switchToHttp().getRequest(); + const user = request.user; + + if (!user) throw new ForbiddenException(AuthMessage.UNAUTHORIZED_ACCESS); + + const hasPermission = requiredPermissions.every((perm) => user.permissions.includes(perm)); + + if (!hasPermission) throw new ForbiddenException(AuthMessage.UNAUTHORIZED_ACCESS); + + return true; + } +} diff --git a/src/modules/auth/guards/role.guard.ts b/src/modules/auth/guards/role.guard.ts index cb13e1c..692d3fd 100644 --- a/src/modules/auth/guards/role.guard.ts +++ b/src/modules/auth/guards/role.guard.ts @@ -19,7 +19,7 @@ export class RoleGuard implements CanActivate { if (!user) throw new ForbiddenException(AuthMessage.UNAUTHORIZED_ACCESS); - const hasRequiredRole = requiredRole.includes(user.role as unknown as RoleEnum); + const hasRequiredRole = requiredRole.some((role) => user.roles.includes(role)); if (!hasRequiredRole) throw new ForbiddenException(AuthMessage.UNAUTHORIZED_ACCESS); return true; diff --git a/src/modules/auth/interfaces/IToken-payload.ts b/src/modules/auth/interfaces/IToken-payload.ts index ce50705..f694a0e 100644 --- a/src/modules/auth/interfaces/IToken-payload.ts +++ b/src/modules/auth/interfaces/IToken-payload.ts @@ -1,6 +1,8 @@ +import { PermissionEnum } from "../../users/enums/permission.enum"; import { RoleEnum } from "../../users/enums/role.enum"; export interface ITokenPayload { - sub: string; - role: RoleEnum; + id: string; + roles: RoleEnum[]; + permissions: PermissionEnum[]; } diff --git a/src/modules/auth/providers/auth.service.ts b/src/modules/auth/providers/auth.service.ts index bcf0811..5294ee2 100644 --- a/src/modules/auth/providers/auth.service.ts +++ b/src/modules/auth/providers/auth.service.ts @@ -4,8 +4,10 @@ import { DataSource } from "typeorm"; import { TokensService } from "./tokens.service"; import { AuthMessage, UserMessage } from "../../../common/enums/message.enum"; import { NotificationsService } from "../../notifications/providers/notifications.service"; +import { User } from "../../users/entities/user.entity"; import { RoleEnum } from "../../users/enums/role.enum"; import { UsersService } from "../../users/providers/users.service"; +import { checkUserRole } from "../../utils/providers/checkRole.utils"; import { OTPService } from "../../utils/providers/otp.service"; import { PasswordService } from "../../utils/providers/password.service"; import { SmsService } from "../../utils/providers/sms.service"; @@ -68,7 +70,7 @@ export class AuthService { const hashedPassword = await this.passwordService.hashPassword(completeRegistrationDto.password); const user = await this.usersService.createUser(completeRegistrationDto, hashedPassword, queryRunner); - const tokens = this.tokensService.generateAccessAndRefreshToken({ sub: user.id, role: user.role.name }); + const tokens = this.generateAccessAndRefreshToken(user); await queryRunner.commitTransaction(); @@ -90,7 +92,7 @@ export class AuthService { const user = await this.checkUserLoginCredentialWithEmail(email, password); - const tokens = this.tokensService.generateAccessAndRefreshToken({ sub: user.id, role: user.role.name }); + const tokens = this.generateAccessAndRefreshToken(user); await this.notificationService.createLoginNotification(user.id); @@ -107,8 +109,10 @@ export class AuthService { const user = await this.checkUserLoginCredentialWithEmail(email, password); - if (user.role.name !== RoleEnum.ADMIN) throw new BadRequestException(AuthMessage.NOT_ADMIN); - const tokens = this.tokensService.generateAccessAndRefreshToken({ sub: user.id, role: user.role.name }); + const hasAccess = checkUserRole(user.roles, RoleEnum.ADMIN); + if (!hasAccess) throw new BadRequestException(AuthMessage.NOT_ADMIN); + + const tokens = this.generateAccessAndRefreshToken(user); return { message: AuthMessage.PASSWORD_LOGIN_SUCCESS, @@ -134,7 +138,9 @@ export class AuthService { if (!user) throw new BadRequestException(AuthMessage.PHONE_NOT_FOUND); //check the if the method call is from admin or not - if (isAdmin && user.role.name !== RoleEnum.ADMIN) throw new BadRequestException(AuthMessage.NOT_ADMIN); + + const hasAccess = checkUserRole(user.roles, RoleEnum.ADMIN); + if (isAdmin && !hasAccess) throw new BadRequestException(AuthMessage.NOT_ADMIN); const existCode = await this.otpService.checkExistOtp(phone, "LOGIN"); if (existCode) { @@ -163,7 +169,7 @@ export class AuthService { const user = await this.checkUserLoginCredentialWithPhone(phone, code); - const tokens = this.tokensService.generateAccessAndRefreshToken({ sub: user.id, role: user.role.name }); + const tokens = this.generateAccessAndRefreshToken(user); await this.notificationService.createLoginNotification(user.id); return { @@ -180,9 +186,12 @@ export class AuthService { const user = await this.checkUserLoginCredentialWithPhone(phone, code); - if (user.role.name !== RoleEnum.ADMIN) throw new BadRequestException(AuthMessage.NOT_ADMIN); + const hasAccess = checkUserRole(user.roles, RoleEnum.ADMIN); + + if (!hasAccess) throw new BadRequestException(AuthMessage.NOT_ADMIN); + + const tokens = this.generateAccessAndRefreshToken(user); - const tokens = this.tokensService.generateAccessAndRefreshToken({ sub: user.id, role: user.role.name }); return { message: AuthMessage.LOGIN_SUCCESS, ...tokens, @@ -231,4 +240,13 @@ export class AuthService { return user; } + //****************** */ + //****************** */ + private generateAccessAndRefreshToken(user: User) { + return this.tokensService.generateAccessAndRefreshToken({ + id: user.id, + roles: user.roles.map((r) => r.name), + permissions: user.roles.flatMap((r) => (r?.permissions?.length ? r.permissions.map((p) => p.name) : [])), + }); + } } diff --git a/src/modules/auth/strategies/jwt.strategy.ts b/src/modules/auth/strategies/jwt.strategy.ts index 1bf5ede..d6b6e6b 100644 --- a/src/modules/auth/strategies/jwt.strategy.ts +++ b/src/modules/auth/strategies/jwt.strategy.ts @@ -17,6 +17,6 @@ export class JwtStrategy extends PassportStrategy(Strategy, JWT_STRATEGY_NAME) { } async validate(payload: ITokenPayload) { - return { id: payload.sub, role: payload.role }; + return { id: payload.id, roles: payload.roles, permissions: payload.permissions }; } } diff --git a/src/modules/danak-services/danak-services.controller.ts b/src/modules/danak-services/danak-services.controller.ts index 0e00351..7557df6 100644 --- a/src/modules/danak-services/danak-services.controller.ts +++ b/src/modules/danak-services/danak-services.controller.ts @@ -106,7 +106,7 @@ export class DanakServicesController { @AuthGuards() @Roles(RoleEnum.USER, RoleEnum.ADMIN) @Get(":id") - getDanakServiceById(@Param() paramDto: ParamDto, @UserDec("role") userRole: RoleEnum) { + getDanakServiceById(@Param() paramDto: ParamDto, @UserDec("roles") userRole: RoleEnum[]) { return this.danakServicesService.getDanakServiceByIdWithSubs(paramDto.id, userRole); } diff --git a/src/modules/danak-services/providers/danak-services.service.ts b/src/modules/danak-services/providers/danak-services.service.ts index d8bd5a0..be78fd5 100644 --- a/src/modules/danak-services/providers/danak-services.service.ts +++ b/src/modules/danak-services/providers/danak-services.service.ts @@ -173,9 +173,10 @@ export class DanakServicesService { } /******************************************** */ - async getDanakServiceByIdWithSubs(serviceId: string, role: RoleEnum) { + async getDanakServiceByIdWithSubs(serviceId: string, roles: RoleEnum[]) { + const hasAccess = roles.includes(RoleEnum.ADMIN); const danakService = await this.danakServicesRepository.findOne({ - where: { id: serviceId, ...(role !== RoleEnum.ADMIN && { isActive: true, subscriptionPlans: { isActive: true } }) }, + where: { id: serviceId, ...(hasAccess && { isActive: true, subscriptionPlans: { isActive: true } }) }, relations: { images: true, subscriptionPlans: true }, }); if (!danakService) throw new BadRequestException(ServiceMessage.SERVICE_NOT_FOUND_BY_ID); diff --git a/src/modules/invoices/invoices.controller.ts b/src/modules/invoices/invoices.controller.ts index 3063216..2a2a600 100644 --- a/src/modules/invoices/invoices.controller.ts +++ b/src/modules/invoices/invoices.controller.ts @@ -46,8 +46,8 @@ export class InvoicesController { @AuthGuards() @Roles(RoleEnum.ADMIN, RoleEnum.USER) @Get(":id") - getInvoiceById(@Param() paramDto: ParamDto, @UserDec("role") role: RoleEnum, @UserDec("id") userId: string) { - return this.invoiceService.getInvoiceById(paramDto.id, role, userId); + getInvoiceById(@Param() paramDto: ParamDto, @UserDec("roles") roles: RoleEnum[], @UserDec("id") userId: string) { + return this.invoiceService.getInvoiceById(paramDto.id, roles, userId); } @Post(":id/apply-discount") diff --git a/src/modules/invoices/providers/invoices.service.ts b/src/modules/invoices/providers/invoices.service.ts index 1d87675..841be16 100644 --- a/src/modules/invoices/providers/invoices.service.ts +++ b/src/modules/invoices/providers/invoices.service.ts @@ -126,11 +126,10 @@ export class InvoicesService { } //*********************************** */ - - async getInvoiceById(invoiceId: string, role: RoleEnum, userId: string) { + async getInvoiceById(invoiceId: string, roles: RoleEnum[], userId: string) { let invoice: Invoice | null; - if (role === RoleEnum.ADMIN) { + if (roles.includes(RoleEnum.ADMIN)) { invoice = await this.invoiceRepository.findOne({ where: { id: invoiceId }, relations: { items: { subscriptionPlan: true }, user: true }, diff --git a/src/modules/payments/payments.controller.ts b/src/modules/payments/payments.controller.ts index 90ebe53..8ad63bc 100644 --- a/src/modules/payments/payments.controller.ts +++ b/src/modules/payments/payments.controller.ts @@ -15,7 +15,6 @@ import { Roles } from "../../common/decorators/roles.decorator"; import { UserDec } from "../../common/decorators/user.decorator"; import { PaginationDto } from "../../common/DTO/pagination.dto"; import { ParamDto } from "../../common/DTO/param.dto"; -import { Role } from "../users/entities/role.entity"; import { RoleEnum } from "../users/enums/role.enum"; @Controller("payments") @@ -124,8 +123,8 @@ export class PaymentsController { @Roles(RoleEnum.ADMIN, RoleEnum.USER) @ApiOperation({ summary: "get bank account" }) @Get("bank-account") - getBankAccounts(@UserDec("role") userRole: Role) { - return this.paymentsService.getBankAccounts(userRole); + getBankAccounts(@UserDec("roles") roles: RoleEnum[]) { + return this.paymentsService.getBankAccounts(roles); } ///------------------- Deposit transfer ----------------------------- diff --git a/src/modules/payments/providers/payments.service.ts b/src/modules/payments/providers/payments.service.ts index 8993207..241fb3c 100644 --- a/src/modules/payments/providers/payments.service.ts +++ b/src/modules/payments/providers/payments.service.ts @@ -7,7 +7,6 @@ import { DataSource, Not, QueryRunner } from "typeorm"; import { PaginationDto } from "../../../common/DTO/pagination.dto"; import { CommonMessage, PaymentMessage, UserMessage, WalletMessage } from "../../../common/enums/message.enum"; -import { Role } from "../../users/entities/role.entity"; import { User } from "../../users/entities/user.entity"; import { RoleEnum } from "../../users/enums/role.enum"; import { PaginationUtils } from "../../utils/providers/pagination.utils"; @@ -263,11 +262,10 @@ export class PaymentsService { await this.bankAccountsRepository.save({ ...bankAccount, ...updateDto }); } //*********************************** */ - async getBankAccounts(role: Role) { - const userRole = role as unknown as RoleEnum; + async getBankAccounts(roles: RoleEnum[]) { let bankAccounts: BankAccount[]; - if (userRole === RoleEnum.USER) { + if (roles.includes(RoleEnum.USER)) { bankAccounts = await this.bankAccountsRepository.find({ where: { isActive: true } }); } else { bankAccounts = await this.bankAccountsRepository.find(); diff --git a/src/modules/tickets/providers/tickets.service.ts b/src/modules/tickets/providers/tickets.service.ts index 7be5b6f..a3a7518 100644 --- a/src/modules/tickets/providers/tickets.service.ts +++ b/src/modules/tickets/providers/tickets.service.ts @@ -3,7 +3,6 @@ import { DataSource, Not } from "typeorm"; import { ParamDto } from "../../../common/DTO/param.dto"; import { CommonMessage, TicketMessageEnum, UserMessage } from "../../../common/enums/message.enum"; -import { Role } from "../../users/entities/role.entity"; import { User } from "../../users/entities/user.entity"; import { RoleEnum } from "../../users/enums/role.enum"; import { UsersService } from "../../users/providers/users.service"; @@ -182,16 +181,15 @@ export class TicketsService { //******************************** */ - async createTicketMessage(ticketId: string, createDto: CreateTicketMessageDto, userId: string, role: Role) { + async createTicketMessage(ticketId: string, createDto: CreateTicketMessageDto, userId: string, roles: RoleEnum[]) { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { - const userRole = role as unknown as RoleEnum; let ticket: null | Ticket = null; - if (userRole === RoleEnum.ADMIN) { + if (roles.includes(RoleEnum.ADMIN)) { ticket = await queryRunner.manager.findOneBy(Ticket, { id: ticketId }); } else { ticket = await queryRunner.manager.findOneBy(Ticket, { id: ticketId, user: { id: userId } }); @@ -206,7 +204,7 @@ export class TicketsService { const ticketMessage = queryRunner.manager.create(TicketMessage, { ...createDto, author: user, ticket }); - if (userRole === RoleEnum.ADMIN && ticket.status === TicketStatus.PENDING) { + if (roles.includes(RoleEnum.ADMIN) && ticket.status === TicketStatus.PENDING) { ticket.status = TicketStatus.ANSWERED; } @@ -236,12 +234,11 @@ export class TicketsService { //******************************** */ - async getTicketMessages(ticketId: string, userId: string, role: Role) { - const userRole = role as unknown as RoleEnum; + async getTicketMessages(ticketId: string, userId: string, roles: RoleEnum[]) { let ticket: null | Ticket = null; // - if (userRole === RoleEnum.ADMIN) { + if (roles.includes(RoleEnum.ADMIN)) { ticket = await this.ticketsRepository.findTicketById(ticketId); } else { ticket = await this.ticketsRepository.findTicketById(ticketId, userId); @@ -257,11 +254,10 @@ export class TicketsService { //******************************** */ - async closeTicketByUser(ticketId: string, userId: string, role: Role) { - const userRole = role as unknown as RoleEnum; + async closeTicketByUser(ticketId: string, userId: string, roles: RoleEnum[]) { let ticket: null | Ticket = null; - if (userRole === RoleEnum.ADMIN) { + if (roles.includes(RoleEnum.ADMIN)) { ticket = await this.ticketsRepository.findTicketById(ticketId); } else { ticket = await this.ticketsRepository.findTicketById(ticketId, userId); diff --git a/src/modules/tickets/repositories/tickets-message.repository.ts b/src/modules/tickets/repositories/tickets-message.repository.ts index 97b6602..fa02d6d 100644 --- a/src/modules/tickets/repositories/tickets-message.repository.ts +++ b/src/modules/tickets/repositories/tickets-message.repository.ts @@ -29,7 +29,7 @@ export class TicketMessagesRepository extends Repository { id: true, firstName: true, lastName: true, - role: { + roles: { id: true, name: false, }, diff --git a/src/modules/tickets/tickets.controller.ts b/src/modules/tickets/tickets.controller.ts index b28ec74..ed4c902 100644 --- a/src/modules/tickets/tickets.controller.ts +++ b/src/modules/tickets/tickets.controller.ts @@ -10,10 +10,12 @@ import { UpdateTicketCategoryDto } from "./DTO/update-ticket-category.dto"; import { TicketsService } from "./providers/tickets.service"; import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; import { Pagination } from "../../common/decorators/pagination.decorator"; +import { PermissionsDec } from "../../common/decorators/permission.decorator"; import { Roles } from "../../common/decorators/roles.decorator"; import { UserDec } from "../../common/decorators/user.decorator"; import { ParamDto } from "../../common/DTO/param.dto"; import { User } from "../users/entities/user.entity"; +import { PermissionEnum } from "../users/enums/permission.enum"; import { RoleEnum } from "../users/enums/role.enum"; @Controller("tickets") @@ -26,6 +28,7 @@ export class TicketsController { @ApiOperation({ summary: "Create ticket category => admin route" }) @AuthGuards() @Roles(RoleEnum.ADMIN) + @PermissionsDec(PermissionEnum.TICKETS) @Post("category") createTicketCategory(@Body() createDto: CreateTicketCategoryDto) { return this.ticketsService.createTicketCategory(createDto); @@ -34,6 +37,7 @@ export class TicketsController { @ApiOperation({ summary: "Update ticket category => admin route" }) @AuthGuards() @Roles(RoleEnum.ADMIN) + @PermissionsDec(PermissionEnum.TICKETS) @Patch("category/:id") updateCategory(@Param() paramDto: ParamDto, @Body() updateCategoryDto: UpdateTicketCategoryDto) { return this.ticketsService.updateCategory(paramDto, updateCategoryDto); @@ -43,6 +47,7 @@ export class TicketsController { @AuthGuards() @Roles(RoleEnum.ADMIN, RoleEnum.USER) @Get("categories") + @PermissionsDec(PermissionEnum.TICKETS) getTicketCategories() { return this.ticketsService.getTicketCategories(); } @@ -98,7 +103,7 @@ export class TicketsController { @Roles(RoleEnum.USER, RoleEnum.ADMIN) @Post(":id/messages") createTicketMessage(@Param() paramDto: ParamDto, @Body() createDto: CreateTicketMessageDto, @UserDec() user: User) { - return this.ticketsService.createTicketMessage(paramDto.id, createDto, user.id, user.role); + return this.ticketsService.createTicketMessage(paramDto.id, createDto, user.id, user.roles as unknown as RoleEnum[]); } @ApiOperation({ summary: "Get all ticket messages of user" }) @@ -106,7 +111,7 @@ export class TicketsController { @Roles(RoleEnum.USER, RoleEnum.ADMIN) @Get(":id/messages") getTicketMessages(@Param() paramDto: ParamDto, @UserDec() user: User) { - return this.ticketsService.getTicketMessages(paramDto.id, user.id, user.role); + return this.ticketsService.getTicketMessages(paramDto.id, user.id, user.roles as unknown as RoleEnum[]); } @ApiOperation({ summary: "close ticket by user or admin" }) @@ -115,6 +120,6 @@ export class TicketsController { @HttpCode(HttpStatus.OK) @Post(":id/close") closedTicketByUser(@Param() paramDto: ParamDto, @UserDec() user: User) { - return this.ticketsService.closeTicketByUser(paramDto.id, user.id, user.role); + return this.ticketsService.closeTicketByUser(paramDto.id, user.id, user.roles as unknown as RoleEnum[]); } } diff --git a/src/modules/users/entities/permission.entity.ts b/src/modules/users/entities/permission.entity.ts new file mode 100644 index 0000000..f93290c --- /dev/null +++ b/src/modules/users/entities/permission.entity.ts @@ -0,0 +1,14 @@ +import { Column, Entity, ManyToMany } from "typeorm"; + +import { Role } from "./role.entity"; +import { BaseEntity } from "../../../common/entities/base.entity"; +import { PermissionEnum } from "../enums/permission.enum"; + +@Entity() +export class Permission extends BaseEntity { + @Column({ type: "enum", enum: PermissionEnum, nullable: false, unique: true }) + name: PermissionEnum; + + @ManyToMany(() => Role, (role) => role.permissions) + roles: Role[]; +} diff --git a/src/modules/users/entities/role.entity.ts b/src/modules/users/entities/role.entity.ts index 2a02ac1..3054f43 100644 --- a/src/modules/users/entities/role.entity.ts +++ b/src/modules/users/entities/role.entity.ts @@ -1,5 +1,6 @@ -import { Column, Entity } from "typeorm"; +import { Column, Entity, JoinTable, ManyToMany } from "typeorm"; +import { Permission } from "./permission.entity"; import { BaseEntity } from "../../../common/entities/base.entity"; import { RoleEnum } from "../enums/role.enum"; @@ -7,4 +8,8 @@ import { RoleEnum } from "../enums/role.enum"; export class Role extends BaseEntity { @Column({ type: "enum", enum: RoleEnum, default: RoleEnum.USER, nullable: false, unique: true }) name: RoleEnum; + + @ManyToMany(() => Permission, (permission) => permission.roles) + @JoinTable({ name: "role_permission_relation" }) + permissions: Permission[]; } diff --git a/src/modules/users/entities/user.entity.ts b/src/modules/users/entities/user.entity.ts index 0465b7a..d6ccc43 100644 --- a/src/modules/users/entities/user.entity.ts +++ b/src/modules/users/entities/user.entity.ts @@ -1,5 +1,5 @@ import { Exclude } from "class-transformer"; -import { Column, Entity, ManyToMany, ManyToOne, OneToMany, OneToOne } from "typeorm"; +import { Column, Entity, JoinTable, ManyToMany, OneToMany, OneToOne } from "typeorm"; import { LegalUser } from "./legal-user.entity"; import { RealUser } from "./real-user.entity"; @@ -47,8 +47,17 @@ export class User extends BaseEntity { nationalCode: string; //----------------------------------------- - @ManyToOne(() => Role, { eager: true, onDelete: "RESTRICT", nullable: false }) - role: Role; + // @ManyToOne(() => Role, { eager: true, onDelete: "RESTRICT", nullable: false }) + // role: Role; + + @ManyToMany(() => Role) + @JoinTable({ name: "user_role_relation" }) + roles: Role[]; + + @ManyToMany(() => UserGroup, (group) => group.users) + groups: UserGroup[]; + + //--------------------------------------- @OneToMany(() => Ticket, (ticket) => ticket.user) tickets: Ticket[]; @@ -56,9 +65,6 @@ export class User extends BaseEntity { @OneToMany(() => TicketMessage, (ticketMessage) => ticketMessage.author) ticketMessage: TicketMessage[]; - @ManyToMany(() => UserGroup, (group) => group.users) - groups: UserGroup[]; - @OneToMany(() => Criticism, (criticism) => criticism.user) criticisms: Criticism[]; diff --git a/src/modules/users/enums/permission.enum.ts b/src/modules/users/enums/permission.enum.ts new file mode 100644 index 0000000..a43909c --- /dev/null +++ b/src/modules/users/enums/permission.enum.ts @@ -0,0 +1,19 @@ +export enum PermissionEnum { + SERVICES = "services", + CUSTOMERS = "customers", + AGENTS = "agents", + DEVELOPERS = "developers", + INVOICES = "invoices", + TRANSACTIONS = "transactions", + DISCOUNTS = "discounts", + ADMINS = "admins", + TICKETS = "tickets", + CRITICISMS = "criticisms", + CONTACTS_US = "contacts_us", + ADVERTISEMENTS = "advertisements", + ANNOUNCEMENTS = "announcements", + BLOGS = "blogs", + LEARNINGS = "learnings", + LOGS = "logs", + SETTINGS = "settings", +} diff --git a/src/modules/users/enums/role.enum.ts b/src/modules/users/enums/role.enum.ts index 84c40e9..5101513 100644 --- a/src/modules/users/enums/role.enum.ts +++ b/src/modules/users/enums/role.enum.ts @@ -1,6 +1,6 @@ export enum RoleEnum { - SUPER_ADMIN = "super_admin", ADMIN = "admin", USER = "user", - VISITOR = "visitor", + DEVELOPER = "developer", + AGENT = "agent", } diff --git a/src/modules/users/providers/users.service.ts b/src/modules/users/providers/users.service.ts index 5b426e7..7255f2b 100644 --- a/src/modules/users/providers/users.service.ts +++ b/src/modules/users/providers/users.service.ts @@ -107,13 +107,13 @@ export class UsersService { /************************************************************ */ async createUser(registerDto: CompleteRegistrationDto, hashedPassword: string, queryRunner: QueryRunner): Promise { - const role = await queryRunner.manager.findOne(Role, { where: { name: RoleEnum.USER } }); + const role = await queryRunner.manager.findOneBy(Role, { name: RoleEnum.USER }); if (!role) throw new BadRequestException(UserMessage.ROLE_NOT_FOUND); const user = queryRunner.manager.create(User, { ...registerDto, password: hashedPassword, - role, + roles: [role], }); await queryRunner.manager.save(user); @@ -264,7 +264,7 @@ export class UsersService { const customer = await this.userRepository.findOne({ where: { id: userId, - role: { + roles: { name: RoleEnum.USER, }, }, diff --git a/src/modules/users/repositories/users.repository.ts b/src/modules/users/repositories/users.repository.ts index 29166f1..4c2f1dc 100644 --- a/src/modules/users/repositories/users.repository.ts +++ b/src/modules/users/repositories/users.repository.ts @@ -11,20 +11,41 @@ export class UserRepository extends Repository { } async findOneWithEmail(email: string): Promise { - return this.findOneBy({ - email, + return this.findOne({ + where: { + email, + }, + relations: { + roles: { + permissions: true, + }, + }, }); } async findOneWithPhone(phone: string): Promise { - return this.findOneBy({ - phone, + return this.findOne({ + where: { + phone, + }, + relations: { + roles: { + permissions: true, + }, + }, }); } async findOneWithUserName(userName: string): Promise { - return this.findOneBy({ - userName, + return this.findOne({ + where: { + userName, + }, + relations: { + roles: { + permissions: true, + }, + }, }); } } diff --git a/src/modules/users/users.module.ts b/src/modules/users/users.module.ts index 7ccd3a1..70b4d2f 100644 --- a/src/modules/users/users.module.ts +++ b/src/modules/users/users.module.ts @@ -2,6 +2,7 @@ import { Module } from "@nestjs/common"; import { TypeOrmModule } from "@nestjs/typeorm"; import { LegalUser } from "./entities/legal-user.entity"; +import { Permission } from "./entities/permission.entity"; import { Role } from "./entities/role.entity"; import { UserGroup } from "./entities/user-group.entity"; import { User } from "./entities/user.entity"; @@ -19,7 +20,7 @@ import { RealUser } from "./entities/real-user.entity"; import { RealUserRepository } from "./repositories/real-user.repository"; @Module({ - imports: [TypeOrmModule.forFeature([User, Role, UserGroup, UserSetting, RealUser, LegalUser]), WalletsModule], + imports: [TypeOrmModule.forFeature([User, Role, UserGroup, UserSetting, RealUser, LegalUser, Permission]), WalletsModule], providers: [ UsersService, UserRepository, diff --git a/src/modules/utils/interfaces/ISms.ts b/src/modules/utils/interfaces/ISms.ts index 248ec39..99c6fdf 100644 --- a/src/modules/utils/interfaces/ISms.ts +++ b/src/modules/utils/interfaces/ISms.ts @@ -23,6 +23,4 @@ export interface ISmsVerifyBody { TemplateId: string; } -// export type SmsBodyType = ISmsVerifyBody; - -export type TemplateParams = "Code"; +export type TemplateParams = "VERIFICATIONCODE"; diff --git a/src/modules/utils/providers/checkRole.utils.ts b/src/modules/utils/providers/checkRole.utils.ts new file mode 100644 index 0000000..b7030d2 --- /dev/null +++ b/src/modules/utils/providers/checkRole.utils.ts @@ -0,0 +1,7 @@ +import { Role } from "../../users/entities/role.entity"; +import { RoleEnum } from "../../users/enums/role.enum"; + +export function checkUserRole(userRoles: Role[], requiredRole: RoleEnum) { + if (!userRoles.some((role) => role.name === requiredRole)) return false; + return true; +} diff --git a/src/modules/utils/providers/sms.service.ts b/src/modules/utils/providers/sms.service.ts index ebd1ba1..43c4563 100644 --- a/src/modules/utils/providers/sms.service.ts +++ b/src/modules/utils/providers/sms.service.ts @@ -20,7 +20,7 @@ export class SmsService { async sendSmsVerifyCode(mobile: string, otpCode: string) { // const smsData: ISmsVerifyBody = { - Parameters: [{ name: "Code", value: otpCode }], + Parameters: [{ name: "VERIFICATIONCODE", value: otpCode }], Mobile: mobile, TemplateId: this.smsConfigs.SMS_PATTERN_OTP, }; @@ -30,7 +30,7 @@ export class SmsService { this.httpService .post( `${this.smsConfigs.API_URL}/send/verify`, - { smsData }, + { ...smsData }, { headers: { "X-API-KEY": this.smsConfigs.API_KEY }, },