diff --git a/src/common/enums/permission.enum.ts b/src/common/enums/permission.enum.ts new file mode 100644 index 0000000..144042e --- /dev/null +++ b/src/common/enums/permission.enum.ts @@ -0,0 +1,46 @@ +/** + * Permission names enum + * All permission names used in the system should be defined here + */ +export enum Permission { + // Food Management + MANAGE_FOODS = 'manage_foods', + MANAGE_CATEGORIES = 'manage_categories', + + // Order Management + MANAGE_ORDERS = 'manage_orders', + + // User & Admin Management + MANAGE_ADMINS = 'manage_admins', + MANAGE_USERS = 'manage_users', + + // Reports & Finances + VIEW_REPORTS = 'view_reports', + MANAGE_FINANCES = 'manage_finances', + + // Restaurant Management + GET_OWN_RESTAURANT = 'get_own_restaurant', + GET_ALL_RESTAURANTS = 'get_all_restaurants', + CREATE_RESTAURANT = 'create_restaurant', + DELETE_RESTAURANT = 'delete_restaurant', + UPDATE_RESTAURANT = 'update_restaurant', +} + +/** + * Permission titles mapping (Farsi) + * Maps permission names to their Farsi titles + */ +export const PermissionTitles: Record = { + [Permission.MANAGE_FOODS]: 'مدیریت غذاها', + [Permission.MANAGE_CATEGORIES]: 'مدیریت دسته‌بندی‌ها', + [Permission.MANAGE_ORDERS]: 'مدیریت سفارشات', + [Permission.MANAGE_ADMINS]: 'مدیریت مدیران', + [Permission.MANAGE_USERS]: 'مدیریت کاربران', + [Permission.VIEW_REPORTS]: 'مشاهده گزارشات', + [Permission.MANAGE_FINANCES]: 'مدیریت مالی', + [Permission.GET_OWN_RESTAURANT]: 'مشاهده رستوران خود', + [Permission.GET_ALL_RESTAURANTS]: 'مشاهده همه رستوران‌ها', + [Permission.CREATE_RESTAURANT]: 'ایجاد رستوران', + [Permission.DELETE_RESTAURANT]: 'حذف رستوران', + [Permission.UPDATE_RESTAURANT]: 'ویرایش رستوران', +}; diff --git a/src/modules/admin/controllers/role.controller.ts b/src/modules/admin/controllers/role.controller.ts index 21e06f1..60a9877 100644 --- a/src/modules/admin/controllers/role.controller.ts +++ b/src/modules/admin/controllers/role.controller.ts @@ -5,6 +5,7 @@ import { CreateRoleDto } from '../dto/create-role.dto'; import { UpdateRoleDto } from '../dto/update-role.dto'; import { FindRolesDto } from '../dto/find-roles.dto'; import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard'; +import { Permissions } from 'src/modules/auth/decorators/permissions.decorator'; @UseGuards(AdminAuthGuard) @ApiBearerAuth() @@ -13,6 +14,7 @@ import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard'; export class RoleController { constructor(private readonly roleService: RoleService) {} + @Permissions('create-role') @Post() @ApiOperation({ summary: 'Create a new role' }) @ApiBody({ type: CreateRoleDto }) diff --git a/src/modules/auth/decorators/permissions.decorator.ts b/src/modules/auth/decorators/permissions.decorator.ts new file mode 100644 index 0000000..a974388 --- /dev/null +++ b/src/modules/auth/decorators/permissions.decorator.ts @@ -0,0 +1,6 @@ + import { SetMetadata } from '@nestjs/common'; + +export const PERMISSIONS_KEY = 'permissions'; + +export const Permissions = (...permissions: string[]) => + SetMetadata(PERMISSIONS_KEY, permissions); diff --git a/src/modules/auth/guards/adminAuth.guard.ts b/src/modules/auth/guards/adminAuth.guard.ts index 6719f24..e6cfe35 100644 --- a/src/modules/auth/guards/adminAuth.guard.ts +++ b/src/modules/auth/guards/adminAuth.guard.ts @@ -1,8 +1,19 @@ -import { CanActivate, ExecutionContext, Injectable, UnauthorizedException, Inject } from '@nestjs/common'; +import { + CanActivate, + ExecutionContext, + Injectable, + UnauthorizedException, + Inject, + Logger, + ForbiddenException, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; import { Request } from 'express'; 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'; export interface AdminAuthRequest extends Request { adminId: string; @@ -11,11 +22,17 @@ export interface AdminAuthRequest extends Request { @Injectable() export class AdminAuthGuard implements CanActivate { + private readonly logger = new Logger(AdminAuthGuard.name); + constructor( @Inject(JwtService) private readonly jwtService: JwtService, @Inject(ConfigService) private readonly configService: ConfigService, + @Inject(ConfigService) + + private readonly adminService: AdminService, + private readonly reflector: Reflector, ) {} async canActivate(context: ExecutionContext) { @@ -28,21 +45,39 @@ export class AdminAuthGuard implements CanActivate { throw new UnauthorizedException(); } - try { - const payload = await this.jwtService.verifyAsync(token, { + const payload = await this.jwtService + .verifyAsync(token, { secret, + }) + .catch(err => { + this.logger.error('error in AdminAuthGuard', err); + throw new UnauthorizedException('Invalid or expired token'); }); - request['adminId'] = payload.adminId; - request['restId'] = payload.restId; - } catch { - throw new UnauthorizedException(); + request['adminId'] = payload.adminId; + request['restId'] = payload.restId; + + // check if the user has the required permissions + const requiredPermissions = + this.reflector.getAllAndOverride(PERMISSIONS_KEY, [context.getHandler(), context.getClass()]) ?? []; + + if (!requiredPermissions || requiredPermissions.length === 0) return true; + + const adminPermission = await this.adminService.getAdminPermissions(payload.adminId, payload.restId); + if (!adminPermission || !Array.isArray(adminPermission)) { + this.logger.error('No permissions found'); + throw new ForbiddenException('No permissions found'); } + + const hasPermission = requiredPermissions.every(p => adminPermission.includes(p)); + + if (!hasPermission) throw new ForbiddenException('You are not authorized to access this resource'); return true; } catch (err) { - console.log('error in AuthGuard', err); + this.logger.error('error in AdminAuthGuard', err); throw new UnauthorizedException('Invalid or expired token'); } } + private extractTokenFromHeader(request: Request): string | undefined { const [type, token] = request.headers.authorization?.split(' ') ?? []; return type === 'Bearer' ? token : undefined; diff --git a/src/modules/auth/guards/auth.guard.ts b/src/modules/auth/guards/auth.guard.ts index 8feb2d0..522ecae 100644 --- a/src/modules/auth/guards/auth.guard.ts +++ b/src/modules/auth/guards/auth.guard.ts @@ -2,7 +2,7 @@ import { CanActivate, ExecutionContext, Injectable, UnauthorizedException, Injec import { Request } from 'express'; import { JwtService } from '@nestjs/jwt'; import { ConfigService } from '@nestjs/config'; -import { IAdminTokenPayload, ITokenPayload } from '../interfaces/IToken-payload'; +import { ITokenPayload } from '../interfaces/IToken-payload'; export interface AdminAuthRequest extends Request { adminId: string; @@ -37,6 +37,7 @@ export class AuthGuard implements CanActivate { } catch { throw new UnauthorizedException(); } + return true; } catch (err) { console.log('error in AuthGuard', err); diff --git a/src/seeders/DatabaseSeeder.ts b/src/seeders/DatabaseSeeder.ts index ac2a779..109b1ae 100644 --- a/src/seeders/DatabaseSeeder.ts +++ b/src/seeders/DatabaseSeeder.ts @@ -3,34 +3,25 @@ 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 { Permission } from '../modules/admin/entities/permission.entity'; +import { Permission as PermissionEntity } from '../modules/admin/entities/permission.entity'; import { Role } from '../modules/admin/entities/role.entity'; import { Restaurant } from '../modules/restaurants/entities/restaurant.entity'; import { User } from '../modules/users/entities/user.entity'; +import { Permission, PermissionTitles } from '../common/enums/permission.enum'; export class DatabaseSeeder extends Seeder { async run(em: EntityManager) { // 1. Create Permissions (English) - const permissions = [ - { name: 'manage_foods', title: 'مدیریت غذاها' }, - { name: 'manage_categories', title: 'مدیریت دسته‌بندی‌ها' }, - { name: 'manage_orders', title: 'مدیریت سفارشات' }, - { name: 'manage_admins', title: 'مدیریت مدیران' }, - { name: 'manage_users', title: 'مدیریت کاربران' }, - { name: 'view_reports', title: 'مشاهده گزارشات' }, - { name: 'manage_finances', title: 'مدیریت مالی' }, - { name: 'get_own_restaurant', title: 'مشاهده رستوران خود' }, - { name: 'get_all_restaurants', title: 'مشاهده همه رستوران‌ها' }, - { name: 'create_restaurant', title: 'ایجاد رستوران' }, - { name: 'delete_restaurant', title: 'حذف رستوران' }, - { name: 'update_restaurant', title: 'ویرایش رستوران' }, - ]; + const permissions = Object.values(Permission).map(name => ({ + name, + title: PermissionTitles[name], + })); - const createdPermissions: Permission[] = []; + const createdPermissions: PermissionEntity[] = []; for (const permData of permissions) { - let permission = await em.findOne(Permission, { name: permData.name }); + let permission = await em.findOne(PermissionEntity, { name: permData.name }); if (!permission) { - permission = em.create(Permission, permData); + permission = em.create(PermissionEntity, permData); em.persist(permission); } createdPermissions.push(permission); @@ -51,7 +42,10 @@ export class DatabaseSeeder extends Seeder { const role = em.create(Role, { name: 'accountant', title: 'حسابدار' }); // Add financial permissions to accountant const financialPerms = createdPermissions.filter( - p => p.name === 'view_reports' || p.name === 'manage_finances' || p.name === 'manage_orders', + p => + p.name === Permission.VIEW_REPORTS || + p.name === Permission.MANAGE_FINANCES || + p.name === Permission.MANAGE_ORDERS, ); financialPerms.forEach(p => role.permissions.add(p)); em.persist(role); @@ -62,7 +56,10 @@ export class DatabaseSeeder extends Seeder { const role = em.create(Role, { name: 'superAdmin', title: 'مدیر کل' }); // Add restaurant management permissions to superAdmin const restaurantPerms = createdPermissions.filter( - p => p.name === 'create_restaurant' || p.name === 'delete_restaurant' || p.name === 'update_restaurant', + p => + p.name === Permission.CREATE_RESTAURANT || + p.name === Permission.DELETE_RESTAURANT || + p.name === Permission.UPDATE_RESTAURANT, ); restaurantPerms.forEach(p => role.permissions.add(p)); em.persist(role);