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.
This commit is contained in:
@@ -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<Request & { adminId?: string }>();
|
||||
return request.adminId || '';
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<AdminRole>(this);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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<Role>(this);
|
||||
}
|
||||
@@ -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<Permission>(this);
|
||||
|
||||
@OneToMany(() => Admin, admin => admin.role)
|
||||
admins = new Collection<Admin>(this);
|
||||
|
||||
@ManyToOne(() => Restaurant, { nullable: true })
|
||||
restaurant?: Restaurant;
|
||||
|
||||
@OneToMany(() => AdminRole, adminRoleRestaurant => adminRoleRestaurant.role)
|
||||
adminRoles = new Collection<AdminRole>(this);
|
||||
}
|
||||
@@ -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 خودداری کن مگر بخوای نام/نوع ستون را سفارشی کنی.
|
||||
}
|
||||
@@ -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<Admin>,
|
||||
@@ -23,24 +21,44 @@ export class AdminService {
|
||||
return this.adminRepository.findOne({ phone });
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<AdminDetailResponse | null> {
|
||||
const admin = await this.adminRepository.findOne({ id }, { populate: ['role', 'role.permissions', 'restaurant'] });
|
||||
async findById(id: string): Promise<Admin | null> {
|
||||
const admin = await this.adminRepository.findOne({ id }, { populate: ['roles', 'roles.role'] });
|
||||
return admin;
|
||||
}
|
||||
|
||||
if (!admin) {
|
||||
return null;
|
||||
async findAllByRestaurantId(restId: string): Promise<Admin[]> {
|
||||
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<Admin[]> {
|
||||
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<string[]> {
|
||||
const cacheKey = `${this.ADMIN_PERMISSIONS_KEY}:${adminId}:${restId}`;
|
||||
|
||||
// Try to get from cache first
|
||||
const cachedPermissions = await this.cacheService.get<string>(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<void> {
|
||||
const cacheKey = `${this.ADMIN_PERMISSIONS_KEY}:${adminId}:${restId}`;
|
||||
await this.cacheService.del(cacheKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,11 @@ export class AdminRepository extends EntityRepository<Admin> {
|
||||
super(em, Admin);
|
||||
}
|
||||
|
||||
async findByPhoneAndrestaurantSlug(phone: string, slug: string): Promise<Admin | null> {
|
||||
async findByPhoneAndRestaurantSlug(phone: string, slug: string): Promise<Admin | null> {
|
||||
return this.em.findOne(
|
||||
Admin,
|
||||
{ phone, restaurant: { slug } },
|
||||
{ populate: ['restaurant', 'role', 'role.permissions'] },
|
||||
{ phone, roles: { role: { restaurant: { slug } } } },
|
||||
{ populate: ['roles', 'roles.role'] },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,49 +19,34 @@ export interface AdminDetailResponse {
|
||||
}
|
||||
|
||||
export class AdminDetailTransformer {
|
||||
static async transform(admin: Admin): Promise<AdminDetailResponse | null> {
|
||||
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
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -17,17 +17,60 @@ export interface AdminLoginResponse {
|
||||
|
||||
export class AdminLoginTransformer {
|
||||
static async transform(admin: Admin, restaurant?: Restaurant): Promise<AdminLoginResponse> {
|
||||
// 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
|
||||
? {
|
||||
|
||||
@@ -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<AdminRole>(this);
|
||||
}
|
||||
|
||||
@@ -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<Permission>,
|
||||
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<string[]> {
|
||||
const cacheKey = `${this.ADMIN_PERMISSIONS_KEY}:${adminId}:${restId}`;
|
||||
|
||||
// Try to get from cache first
|
||||
const cachedPermissions = await this.cacheService.get<string>(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<void> {
|
||||
const cacheKey = `${this.ADMIN_PERMISSIONS_KEY}:${adminId}:${restId}`;
|
||||
await this.cacheService.del(cacheKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user