From 8a217f6034e65613dc181e0d239d0383ffe45faa Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Tue, 18 Nov 2025 16:45:44 +0330 Subject: [PATCH] Add AdminId decorator for extracting adminId from requests; refactor AdminController to utilize new decorator for fetching admin details and restaurant-specific admins; update AdminService to handle admin creation and retrieval by restaurant; remove unused role and permission entities for cleaner architecture. --- src/common/decorators/admin-id.decorator.ts | 18 +++ .../admin/controllers/admin.controller.ts | 14 ++- .../dto/create-my-restaurant-admin.dto.ts | 24 ++++ src/modules/admin/entities/admin.entity.ts | 17 ++- .../admin/entities/adminRole.entity.ts | 7 +- .../admin/entities/permission.entity.ts | 16 --- src/modules/admin/entities/role.entity.ts | 29 ----- .../admin/entities/rolePermission.entity.ts | 17 --- src/modules/admin/providers/admin.service.ts | 104 ++++++------------ .../admin/repositories/admin.repository.ts | 6 +- .../transformers/admin-detail.transformer.ts | 45 +++----- src/modules/auth/guards/adminAuth.guard.ts | 6 +- src/modules/auth/services/auth.service.ts | 4 +- .../transformers/admin-login.transformer.ts | 59 ++++++++-- src/modules/roles/entities/role.entity.ts | 4 + .../roles/providers/permissions.service.ts | 55 +++++++++ src/modules/roles/roles.module.ts | 7 +- src/seeders/DatabaseSeeder.ts | 28 ++++- 18 files changed, 254 insertions(+), 206 deletions(-) create mode 100644 src/common/decorators/admin-id.decorator.ts create mode 100644 src/modules/admin/dto/create-my-restaurant-admin.dto.ts delete mode 100755 src/modules/admin/entities/permission.entity.ts delete mode 100755 src/modules/admin/entities/role.entity.ts delete mode 100644 src/modules/admin/entities/rolePermission.entity.ts diff --git a/src/common/decorators/admin-id.decorator.ts b/src/common/decorators/admin-id.decorator.ts new file mode 100644 index 0000000..2d6d601 --- /dev/null +++ b/src/common/decorators/admin-id.decorator.ts @@ -0,0 +1,18 @@ +import { createParamDecorator, type ExecutionContext } from '@nestjs/common'; +import type { Request } from 'express'; + +/** + * Decorator to extract userId from the authenticated request. + * Must be used after AdminAuthGuard or AuthGuard that sets request.userId. + * + * @example + * @Get('/profile') + * @UseGuards(AdminAuthGuard) + * getProfile(@UserId() userId: string) { + * return this.userService.findById(userId); + * } + */ +export const AdminId = createParamDecorator((data: unknown, ctx: ExecutionContext): string => { + const request = ctx.switchToHttp().getRequest(); + return request.adminId || ''; +}); diff --git a/src/modules/admin/controllers/admin.controller.ts b/src/modules/admin/controllers/admin.controller.ts index 620d23f..fec1346 100644 --- a/src/modules/admin/controllers/admin.controller.ts +++ b/src/modules/admin/controllers/admin.controller.ts @@ -3,6 +3,8 @@ import { AdminService } from '../providers/admin.service'; import { CreateAdminDto } from '../dto/create-admin.dto'; import { ApiTags, ApiOperation, ApiBody, ApiResponse, ApiBearerAuth } from '@nestjs/swagger'; import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard'; +import { RestId } from 'src/common/decorators'; +import { AdminId } from 'src/common/decorators/admin-id.decorator'; @UseGuards(AdminAuthGuard) @ApiBearerAuth() @@ -11,19 +13,19 @@ import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard'; export class AdminController { constructor(private readonly adminService: AdminService) {} - @Get() + @Get('my-restaurant-admins') @ApiOperation({ summary: 'admin' }) @ApiResponse({ status: 201, description: 'Admins' }) - async getAll() { - const admin = await this.adminService.findAll(); + async getAll(@RestId() restId: string) { + const admin = await this.adminService.findAllByRestaurantId(restId); return admin; } @Get('me') @ApiOperation({ summary: 'admin' }) @ApiResponse({ status: 201, description: 'Admins' }) - async getMe() { - const admin = await this.adminService.findAll(); + async getMe(@AdminId() adminId: string) { + const admin = await this.adminService.findById(adminId); return admin; } @@ -33,7 +35,7 @@ export class AdminController { @ApiBody({ type: CreateAdminDto }) @ApiResponse({ status: 201, description: 'Admin created' }) async create(@Body() dto: CreateAdminDto) { - const admin = await this.adminService.create({ + const admin = await this.adminService.createRestaurantBySuperAdmin({ phone: dto.phone, firstName: dto.firstName, lastName: dto.lastName, diff --git a/src/modules/admin/dto/create-my-restaurant-admin.dto.ts b/src/modules/admin/dto/create-my-restaurant-admin.dto.ts new file mode 100644 index 0000000..9387a01 --- /dev/null +++ b/src/modules/admin/dto/create-my-restaurant-admin.dto.ts @@ -0,0 +1,24 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; + +export class CreateMyRestaurantAdminDto { + @ApiProperty({ description: 'Mobile phone number' }) + @IsNotEmpty() + @IsString() + phone!: string; + + @ApiProperty({ description: 'First name', required: false }) + @IsOptional() + @IsString() + firstName?: string; + + @ApiProperty({ description: 'Last name', required: false }) + @IsOptional() + @IsString() + lastName?: string; + + @ApiProperty({ description: 'Role id for the admin' }) + @IsNotEmpty() + @IsString() + roleId!: string; +} diff --git a/src/modules/admin/entities/admin.entity.ts b/src/modules/admin/entities/admin.entity.ts index b3d75cb..27c99e6 100644 --- a/src/modules/admin/entities/admin.entity.ts +++ b/src/modules/admin/entities/admin.entity.ts @@ -1,11 +1,8 @@ -import { Entity, ManyToOne, Property } from '@mikro-orm/core'; +import { Cascade, Collection, Entity, OneToMany, Property } from '@mikro-orm/core'; import { BaseEntity } from '../../../common/entities/base.entity'; -import { Role } from '../../roles/entities/role.entity'; -import { Restaurant } from '../../restaurants/entities/restaurant.entity'; +import { AdminRole } from './adminRole.entity'; @Entity({ tableName: 'admins' }) -// Add the Unique constraint for the combination of 'phone' and 'restaurant' -// @Unique({ properties: ['phone', 'restaurant'] }) export class Admin extends BaseEntity { @Property({ nullable: true }) firstName?: string; @@ -17,9 +14,9 @@ export class Admin extends BaseEntity { phone!: string; // Add the new role property - @ManyToOne(() => Role) - role!: Role; - - @ManyToOne(() => Restaurant, { nullable: true }) - restaurant?: Restaurant; + @OneToMany(() => AdminRole, adminRole => adminRole.admin, { + cascade: [Cascade.ALL], + orphanRemoval: true, + }) + roles = new Collection(this); } diff --git a/src/modules/admin/entities/adminRole.entity.ts b/src/modules/admin/entities/adminRole.entity.ts index 59de879..2b2b296 100644 --- a/src/modules/admin/entities/adminRole.entity.ts +++ b/src/modules/admin/entities/adminRole.entity.ts @@ -1,13 +1,12 @@ -import { Entity, ManyToOne } from '@mikro-orm/core'; +import { Entity, ManyToOne, Unique } from '@mikro-orm/core'; import { BaseEntity } from '../../../common/entities/base.entity'; -import { Role } from './role.entity'; +import { Role } from '../../roles/entities/role.entity'; import { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity'; import { Admin } from './admin.entity'; @Entity({ tableName: 'admin_roles' }) -// Add the Unique constraint for the combination of 'phone' and 'restaurant' -// @Unique({ properties: ['phone', 'restaurant'] }) export class AdminRole extends BaseEntity { + @Unique({ properties: ['admin.id', 'role.id', 'restaurant.id'] }) @ManyToOne(() => Admin) admin!: Admin; diff --git a/src/modules/admin/entities/permission.entity.ts b/src/modules/admin/entities/permission.entity.ts deleted file mode 100755 index e50d417..0000000 --- a/src/modules/admin/entities/permission.entity.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Entity, Property, Unique, ManyToMany, Collection } from '@mikro-orm/core'; -import { BaseEntity } from '../../../common/entities/base.entity'; -import { Role } from './role.entity'; - -@Entity({ tableName: 'permissions' }) -export class Permission extends BaseEntity { - @Property() - @Unique() - name!: string; - - @Property() - title!: string; - - @ManyToMany({ entity: () => Role, mappedBy: r => r.permissions }) - roles = new Collection(this); -} diff --git a/src/modules/admin/entities/role.entity.ts b/src/modules/admin/entities/role.entity.ts deleted file mode 100755 index bd7ba62..0000000 --- a/src/modules/admin/entities/role.entity.ts +++ /dev/null @@ -1,29 +0,0 @@ -// role.entity.ts -import { Collection, Entity, ManyToMany, OneToMany, ManyToOne, Property } from '@mikro-orm/core'; -import { BaseEntity } from '../../../common/entities/base.entity'; -import { Admin } from './admin.entity'; -import { Permission } from './permission.entity'; -import { Restaurant } from '../../../modules/restaurants/entities/restaurant.entity'; -import { RolePermission } from './rolePermission.entity'; -import { AdminRole } from './adminRole.entity'; - -@Entity({ tableName: 'roles' }) -export class Role extends BaseEntity { - @Property() - name!: string; - - @Property() - title!: string; - - @ManyToMany({ entity: () => Permission, pivotEntity: () => RolePermission, inversedBy: p => p.roles }) - permissions = new Collection(this); - - @OneToMany(() => Admin, admin => admin.role) - admins = new Collection(this); - - @ManyToOne(() => Restaurant, { nullable: true }) - restaurant?: Restaurant; - - @OneToMany(() => AdminRole, adminRoleRestaurant => adminRoleRestaurant.role) - adminRoles = new Collection(this); -} diff --git a/src/modules/admin/entities/rolePermission.entity.ts b/src/modules/admin/entities/rolePermission.entity.ts deleted file mode 100644 index 75d062d..0000000 --- a/src/modules/admin/entities/rolePermission.entity.ts +++ /dev/null @@ -1,17 +0,0 @@ -// role-permission.entity.ts (pivot) -import { Entity, ManyToOne } from '@mikro-orm/core'; -import { Role } from './role.entity'; -import { Permission } from './permission.entity'; - -@Entity({ tableName: 'role_permissions' }) -export class RolePermission { - // دو ManyToOne دقیقاً؛ هر کدام می‌تونن primary: true باشند برای composite PK - @ManyToOne(() => Role, { primary: true }) - role!: Role; - - @ManyToOne(() => Permission, { primary: true }) - permission!: Permission; - - // **نکته:** اینجا *فقط* همین دو خصوصیت کافی است — - // از تعریف جداگانه‌ی roleId / permissionId با @PrimaryKey خودداری کن مگر بخوای نام/نوع ستون را سفارشی کنی. -} diff --git a/src/modules/admin/providers/admin.service.ts b/src/modules/admin/providers/admin.service.ts index 305050a..5e36630 100644 --- a/src/modules/admin/providers/admin.service.ts +++ b/src/modules/admin/providers/admin.service.ts @@ -6,12 +6,10 @@ import { Role } from '../../roles/entities/role.entity'; import { Restaurant } from '../../restaurants/entities/restaurant.entity'; import { EntityManager } from '@mikro-orm/postgresql'; import { CacheService } from '../../utils/cache.service'; -import { AdminDetailTransformer, AdminDetailResponse } from '../transformers/admin-detail.transformer'; +import { CreateMyRestaurantAdminDto } from '../dto/create-my-restaurant-admin.dto'; @Injectable() export class AdminService { - private readonly ADMIN_PERMISSIONS_KEY = 'admin-perms'; - constructor( @InjectRepository(Admin) private readonly adminRepository: EntityRepository, @@ -23,24 +21,44 @@ export class AdminService { return this.adminRepository.findOne({ phone }); } - async findById(id: string): Promise { - const admin = await this.adminRepository.findOne({ id }, { populate: ['role', 'role.permissions', 'restaurant'] }); + async findById(id: string): Promise { + const admin = await this.adminRepository.findOne({ id }, { populate: ['roles', 'roles.role'] }); + return admin; + } - if (!admin) { - return null; + async findAllByRestaurantId(restId: string): Promise { + return this.adminRepository.find({ roles: { restaurant: { id: restId } } }, { populate: ['roles', 'roles.role'] }); + } + + async createAdminForMyRestaurant(restId: string, dto: CreateMyRestaurantAdminDto) { + const { phone, firstName, lastName, roleId } = dto; + const currentAdmin = await this.adminRepository.findOne({ + phone, + roles: { role: { id: roleId }, restaurant: { id: restId } }, + }); + if (currentAdmin) { + return currentAdmin; } - - return AdminDetailTransformer.transform(admin); - } - async findAll(): Promise { - return this.adminRepository.findAll(); + const role = await this.em.findOne(Role, { id: roleId }); + if (!role) throw new Error('Role not found'); + const restaurant = await this.em.findOne(Restaurant, { id: restId }); + if (!restaurant) throw new Error('Restaurant not found'); + const admin = this.adminRepository.create({ phone, firstName, lastName, roles: [{ role, restaurant }] }); + await this.em.persistAndFlush(admin); + return admin; } - async create(data: { phone: string; firstName?: string; lastName?: string; roleId: string; restId: string }) { + async createRestaurantBySuperAdmin(data: { + phone: string; + firstName?: string; + lastName?: string; + roleId: string; + restId: string; + }) { const { phone, firstName, lastName, roleId, restId } = data; // check existing - const existing = await this.adminRepository.findOne({ phone, restaurant: { id: restId } }); + const existing = await this.adminRepository.findOne({ phone, roles: { restaurant: { id: restId } } }); if (existing) { return existing; } @@ -52,64 +70,8 @@ export class AdminService { const restaurant = await this.em.findOne(Restaurant, { id: restId }); if (!restaurant) throw new Error('Restaurant not found'); - const admin = this.adminRepository.create({ phone, firstName, lastName, role, restaurant }); + const admin = this.adminRepository.create({ phone, firstName, lastName, roles: [{ role, restaurant }] }); await this.em.persistAndFlush(admin); return admin; } - - /** - * Get admin permissions from cache or database - * @param adminId - The admin ID - * @param restId - The restaurant ID - * @returns Array of permission names (string[]) - */ - async getAdminPermissions(adminId: string, restId: string): Promise { - const cacheKey = `${this.ADMIN_PERMISSIONS_KEY}:${adminId}:${restId}`; - - // Try to get from cache first - const cachedPermissions = await this.cacheService.get(cacheKey); - if (cachedPermissions) { - try { - const parsed: unknown = JSON.parse(cachedPermissions); - // Ensure it's an array of strings - if (Array.isArray(parsed) && parsed.every((p): p is string => typeof p === 'string')) { - return parsed; - } - // If invalid format, continue to fetch from DB - } catch { - // If parsing fails, continue to fetch from DB - } - } - - // If not in cache, fetch from database - const admin = await this.adminRepository.findOne( - { id: adminId, restaurant: { id: restId } }, - { populate: ['role', 'role.permissions'] }, - ); - - if (!admin || !admin.role) { - return []; - } - - // Extract permission names as array of strings - const permissions = await admin.role.permissions.loadItems(); - const permissionNames: string[] = permissions - .map(p => p.name) - .filter((name): name is string => typeof name === 'string'); - - // Store in cache as JSON stringified array of strings (1 hour TTL) - await this.cacheService.set(cacheKey, JSON.stringify(permissionNames), 3600); - - return permissionNames; - } - - /** - * Invalidate admin permissions cache - * @param adminId - The admin ID - * @param restId - The restaurant ID - */ - async invalidateAdminPermissionsCache(adminId: string, restId: string): Promise { - const cacheKey = `${this.ADMIN_PERMISSIONS_KEY}:${adminId}:${restId}`; - await this.cacheService.del(cacheKey); - } } diff --git a/src/modules/admin/repositories/admin.repository.ts b/src/modules/admin/repositories/admin.repository.ts index c9b4acd..bc7b282 100644 --- a/src/modules/admin/repositories/admin.repository.ts +++ b/src/modules/admin/repositories/admin.repository.ts @@ -8,11 +8,11 @@ export class AdminRepository extends EntityRepository { super(em, Admin); } - async findByPhoneAndrestaurantSlug(phone: string, slug: string): Promise { + async findByPhoneAndRestaurantSlug(phone: string, slug: string): Promise { return this.em.findOne( Admin, - { phone, restaurant: { slug } }, - { populate: ['restaurant', 'role', 'role.permissions'] }, + { phone, roles: { role: { restaurant: { slug } } } }, + { populate: ['roles', 'roles.role'] }, ); } } diff --git a/src/modules/admin/transformers/admin-detail.transformer.ts b/src/modules/admin/transformers/admin-detail.transformer.ts index b8986af..5a14ba9 100644 --- a/src/modules/admin/transformers/admin-detail.transformer.ts +++ b/src/modules/admin/transformers/admin-detail.transformer.ts @@ -19,49 +19,34 @@ export interface AdminDetailResponse { } export class AdminDetailTransformer { - static async transform(admin: Admin): Promise { + static transform(admin: Admin): AdminDetailResponse | null { if (!admin) { return null; } // Extract role information - const role = admin.role - ? { - id: admin.role.id, - name: admin.role.name, - title: admin.role.title, - } - : null; - + const role = admin.roles.getItems().find(r => r.role)?.role; if (!role) { return null; } - - // Extract permissions - ensure collection is loaded if needed - let permissions: string[] = []; - if (admin.role?.permissions) { - if (admin.role.permissions.isInitialized()) { - permissions = admin.role.permissions.getItems().map(p => p.name); - } else { - await admin.role.permissions.loadItems(); - permissions = admin.role.permissions.getItems().map(p => p.name); - } - } - return { id: admin.id, firstName: admin.firstName, lastName: admin.lastName, phone: admin.phone, - role, - permissions, - restaurant: admin.restaurant - ? { - id: admin.restaurant.id, - name: admin.restaurant.name, - slug: admin.restaurant.slug, - } - : undefined, + role: { + id: role.id, + name: role.name, + title: role.title, + }, + permissions: role.permissions.getItems().map(p => p.name) || [], + restaurant: { + id: role.restaurant?.id || '', + name: role.restaurant?.name || '', + slug: role.restaurant?.slug || '', + }, }; } } + +// Extract permissions - ensure collection is loaded if needed diff --git a/src/modules/auth/guards/adminAuth.guard.ts b/src/modules/auth/guards/adminAuth.guard.ts index e6cfe35..777463b 100644 --- a/src/modules/auth/guards/adminAuth.guard.ts +++ b/src/modules/auth/guards/adminAuth.guard.ts @@ -13,7 +13,7 @@ import { JwtService } from '@nestjs/jwt'; import { ConfigService } from '@nestjs/config'; import { IAdminTokenPayload } from '../interfaces/IToken-payload'; import { PERMISSIONS_KEY } from '../decorators/permissions.decorator'; -import { AdminService } from 'src/modules/admin/providers/admin.service'; + import { PermissionsService } from 'src/modules/roles/providers/permissions.service'; export interface AdminAuthRequest extends Request { adminId: string; @@ -31,7 +31,7 @@ export class AdminAuthGuard implements CanActivate { private readonly configService: ConfigService, @Inject(ConfigService) - private readonly adminService: AdminService, + private readonly permissionsService: PermissionsService, private readonly reflector: Reflector, ) {} @@ -62,7 +62,7 @@ export class AdminAuthGuard implements CanActivate { if (!requiredPermissions || requiredPermissions.length === 0) return true; - const adminPermission = await this.adminService.getAdminPermissions(payload.adminId, payload.restId); + const adminPermission = await this.permissionsService.getAdminPermissions(payload.adminId, payload.restId); if (!adminPermission || !Array.isArray(adminPermission)) { this.logger.error('No permissions found'); throw new ForbiddenException('No permissions found'); diff --git a/src/modules/auth/services/auth.service.ts b/src/modules/auth/services/auth.service.ts index c265383..31b5704 100644 --- a/src/modules/auth/services/auth.service.ts +++ b/src/modules/auth/services/auth.service.ts @@ -38,7 +38,7 @@ export class AuthService { async requestOtpAdmin(dto: RequestOtpDto) { const { phone, slug } = dto; - const admin = await this.adminRepository.findByPhoneAndrestaurantSlug(phone, slug); + const admin = await this.adminRepository.findByPhoneAndRestaurantSlug(phone, slug); if (!admin) { throw new NotFoundException(); } @@ -83,7 +83,7 @@ export class AuthService { await this.cacheService.del(`otp-admin:${slug}:${phone}`); - const admin = await this.adminRepository.findByPhoneAndrestaurantSlug(phone, slug); + const admin = await this.adminRepository.findByPhoneAndRestaurantSlug(phone, slug); if (!admin) { throw new BadRequestException(AuthMessage.USER_NOT_FOUND); diff --git a/src/modules/auth/transformers/admin-login.transformer.ts b/src/modules/auth/transformers/admin-login.transformer.ts index d30216b..c58ed4b 100644 --- a/src/modules/auth/transformers/admin-login.transformer.ts +++ b/src/modules/auth/transformers/admin-login.transformer.ts @@ -17,17 +17,60 @@ export interface AdminLoginResponse { export class AdminLoginTransformer { static async transform(admin: Admin, restaurant?: Restaurant): Promise { - // Extract role name - const role = admin.role?.name || ''; + // Find the AdminRole that matches the restaurant (or get the first one) + let adminRole = admin.roles.getItems().find(r => (restaurant ? r.restaurant?.id === restaurant.id : true)); + + // If no match found, get the first one + if (!adminRole && admin.roles.getItems().length > 0) { + adminRole = admin.roles.getItems()[0]; + } + + if (!adminRole) { + return { + id: admin.id, + firstName: admin.firstName, + lastName: admin.lastName, + phone: admin.phone, + role: '', + permissions: [], + restaurant: restaurant + ? { + id: restaurant.id, + name: restaurant.name, + slug: restaurant.slug, + } + : undefined, + }; + } + + // Get the role from AdminRole + const role = adminRole.role; + if (!role) { + return { + id: admin.id, + firstName: admin.firstName, + lastName: admin.lastName, + phone: admin.phone, + role: '', + permissions: [], + restaurant: restaurant + ? { + id: restaurant.id, + name: restaurant.name, + slug: restaurant.slug, + } + : undefined, + }; + } // Extract permissions - ensure collection is loaded if needed let permissions: string[] = []; - if (admin.role?.permissions) { - if (admin.role.permissions.isInitialized()) { - permissions = admin.role.permissions.getItems().map(p => p.name); + if (role.permissions) { + if (role.permissions.isInitialized()) { + permissions = role.permissions.getItems().map(p => p.name); } else { - await admin.role.permissions.loadItems(); - permissions = admin.role.permissions.getItems().map(p => p.name); + await role.permissions.loadItems(); + permissions = role.permissions.getItems().map(p => p.name); } } @@ -36,7 +79,7 @@ export class AdminLoginTransformer { firstName: admin.firstName, lastName: admin.lastName, phone: admin.phone, - role, + role: role.name, permissions, restaurant: restaurant ? { diff --git a/src/modules/roles/entities/role.entity.ts b/src/modules/roles/entities/role.entity.ts index 3c92c77..70c0707 100755 --- a/src/modules/roles/entities/role.entity.ts +++ b/src/modules/roles/entities/role.entity.ts @@ -4,6 +4,7 @@ import { BaseEntity } from '../../../common/entities/base.entity'; import { Permission } from './permission.entity'; import { Restaurant } from '../../restaurants/entities/restaurant.entity'; import { RolePermission } from './rolePermission.entity'; +import { AdminRole } from 'src/modules/admin/entities/adminRole.entity'; @Entity({ tableName: 'roles' }) export class Role extends BaseEntity { @@ -18,4 +19,7 @@ export class Role extends BaseEntity { @ManyToOne(() => Restaurant, { nullable: true }) restaurant?: Restaurant; + + @OneToMany(() => AdminRole, adminRole => adminRole.role) + admins = new Collection(this); } diff --git a/src/modules/roles/providers/permissions.service.ts b/src/modules/roles/providers/permissions.service.ts index 6b4a7c7..421d320 100644 --- a/src/modules/roles/providers/permissions.service.ts +++ b/src/modules/roles/providers/permissions.service.ts @@ -2,16 +2,71 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@mikro-orm/nestjs'; import { EntityRepository } from '@mikro-orm/core'; import { Permission } from '../entities/permission.entity'; +import { CacheService } from 'src/modules/utils/cache.service'; +import { AdminRepository } from 'src/modules/admin/repositories/admin.repository'; @Injectable() export class PermissionsService { + private readonly ADMIN_PERMISSIONS_KEY = 'admin-perms'; + constructor( @InjectRepository(Permission) private readonly permissionRepository: EntityRepository, + private readonly cacheService: CacheService, + private readonly adminRepository: AdminRepository, ) {} async findAll() { const permissions = await this.permissionRepository.findAll(); return permissions; } + + /** + * Get admin permissions from cache or database + * @param adminId - The admin ID + * @param restId - The restaurant ID + * @returns Array of permission names (string[]) + */ + async getAdminPermissions(adminId: string, restId: string): Promise { + const cacheKey = `${this.ADMIN_PERMISSIONS_KEY}:${adminId}:${restId}`; + + // Try to get from cache first + const cachedPermissions = await this.cacheService.get(cacheKey); + if (cachedPermissions) { + try { + const parsed: unknown = JSON.parse(cachedPermissions); + // Ensure it's an array of strings + if (Array.isArray(parsed) && parsed.every((p): p is string => typeof p === 'string')) { + return parsed; + } + // If invalid format, continue to fetch from DB + } catch { + // If parsing fails, continue to fetch from DB + } + } + + // If not in cache, fetch from database + const admin = await this.adminRepository.findOne( + { id: adminId, roles: { restaurant: { id: restId } } }, + { populate: ['roles', 'roles.role'] }, + ); + + if (!admin || !admin.roles) { + return []; + } + + // Extract permission names as array of strings + const permissions = admin.roles.map(r => r.role.permissions.getItems()); + return permissions.flat().map(p => p.name); + } + + /** + * Invalidate admin permissions cache + * @param adminId - The admin ID + * @param restId - The restaurant ID + */ + async invalidateAdminPermissionsCache(adminId: string, restId: string): Promise { + const cacheKey = `${this.ADMIN_PERMISSIONS_KEY}:${adminId}:${restId}`; + await this.cacheService.del(cacheKey); + } } diff --git a/src/modules/roles/roles.module.ts b/src/modules/roles/roles.module.ts index 474ab8e..ef289ac 100644 --- a/src/modules/roles/roles.module.ts +++ b/src/modules/roles/roles.module.ts @@ -7,12 +7,11 @@ import { Role } from './entities/role.entity'; import { Permission } from './entities/permission.entity'; import { RolePermission } from './entities/rolePermission.entity'; import { JwtModule } from '@nestjs/jwt'; +import { UtilsModule } from '../utils/utils.module'; +import { AdminModule } from '../admin/admin.module'; @Module({ - imports: [ - MikroOrmModule.forFeature([Role, Permission, RolePermission]), - JwtModule, - ], + imports: [MikroOrmModule.forFeature([Role, Permission, RolePermission]), JwtModule, UtilsModule, AdminModule], controllers: [RolesController], providers: [RolesService, PermissionsService], exports: [RolesService, PermissionsService], diff --git a/src/seeders/DatabaseSeeder.ts b/src/seeders/DatabaseSeeder.ts index 11ed201..c589a57 100644 --- a/src/seeders/DatabaseSeeder.ts +++ b/src/seeders/DatabaseSeeder.ts @@ -3,6 +3,7 @@ import { Seeder } from '@mikro-orm/seeder'; import { Category } from '../modules/foods/entities/category.entity'; import { Food } from '../modules/foods/entities/food.entity'; import { Admin } from '../modules/admin/entities/admin.entity'; +import { AdminRole } from '../modules/admin/entities/adminRole.entity'; import { Permission as PermissionEntity } from '../modules/roles/entities/permission.entity'; import { Role } from '../modules/roles/entities/role.entity'; import { Restaurant } from '../modules/restaurants/entities/restaurant.entity'; @@ -421,10 +422,17 @@ export class DatabaseSeeder extends Seeder { phone: '09362532122', firstName: 'مرتضی', lastName: 'مرتضایی', + }); + em.persist(admin); + + // Create AdminRole linking admin to role and restaurant + const adminRole = em.create(AdminRole, { + admin, role: restaurantRole, restaurant: zhivan, }); - em.persist(admin); + em.persist(adminRole); + admin.roles.add(adminRole); } const adminHamid = await em.findOne(Admin, { phone: '09185290775' }); @@ -433,10 +441,17 @@ export class DatabaseSeeder extends Seeder { phone: '09185290775', firstName: 'حمید', lastName: 'زرگامی', + }); + em.persist(admin); + + // Create AdminRole linking admin to role and restaurant + const adminRole = em.create(AdminRole, { + admin, role: accountant, restaurant: boote, }); - em.persist(admin); + em.persist(adminRole); + admin.roles.add(adminRole); } const adminMehrdad = await em.findOne(Admin, { phone: '0912928395' }); @@ -445,10 +460,17 @@ export class DatabaseSeeder extends Seeder { phone: '0912928395', firstName: 'مهرداد', lastName: 'مظفری', + }); + em.persist(admin); + + // Create AdminRole linking admin to role (no restaurant for superAdmin) + const adminRole = em.create(AdminRole, { + admin, role: superAdmin, restaurant: null, // SuperAdmin doesn't belong to a specific restaurant }); - em.persist(admin); + em.persist(adminRole); + admin.roles.add(adminRole); } await em.flush();