Add permissions handling to AdminAuthGuard and RoleController; refactor DatabaseSeeder to utilize enums for permissions
This commit is contained in:
@@ -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, string> = {
|
||||||
|
[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]: 'ویرایش رستوران',
|
||||||
|
};
|
||||||
@@ -5,6 +5,7 @@ import { CreateRoleDto } from '../dto/create-role.dto';
|
|||||||
import { UpdateRoleDto } from '../dto/update-role.dto';
|
import { UpdateRoleDto } from '../dto/update-role.dto';
|
||||||
import { FindRolesDto } from '../dto/find-roles.dto';
|
import { FindRolesDto } from '../dto/find-roles.dto';
|
||||||
import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard';
|
import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard';
|
||||||
|
import { Permissions } from 'src/modules/auth/decorators/permissions.decorator';
|
||||||
|
|
||||||
@UseGuards(AdminAuthGuard)
|
@UseGuards(AdminAuthGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@@ -13,6 +14,7 @@ import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard';
|
|||||||
export class RoleController {
|
export class RoleController {
|
||||||
constructor(private readonly roleService: RoleService) {}
|
constructor(private readonly roleService: RoleService) {}
|
||||||
|
|
||||||
|
@Permissions('create-role')
|
||||||
@Post()
|
@Post()
|
||||||
@ApiOperation({ summary: 'Create a new role' })
|
@ApiOperation({ summary: 'Create a new role' })
|
||||||
@ApiBody({ type: CreateRoleDto })
|
@ApiBody({ type: CreateRoleDto })
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { SetMetadata } from '@nestjs/common';
|
||||||
|
|
||||||
|
export const PERMISSIONS_KEY = 'permissions';
|
||||||
|
|
||||||
|
export const Permissions = (...permissions: string[]) =>
|
||||||
|
SetMetadata(PERMISSIONS_KEY, permissions);
|
||||||
@@ -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 { Request } from 'express';
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { IAdminTokenPayload } from '../interfaces/IToken-payload';
|
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 {
|
export interface AdminAuthRequest extends Request {
|
||||||
adminId: string;
|
adminId: string;
|
||||||
@@ -11,11 +22,17 @@ export interface AdminAuthRequest extends Request {
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AdminAuthGuard implements CanActivate {
|
export class AdminAuthGuard implements CanActivate {
|
||||||
|
private readonly logger = new Logger(AdminAuthGuard.name);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(JwtService)
|
@Inject(JwtService)
|
||||||
private readonly jwtService: JwtService,
|
private readonly jwtService: JwtService,
|
||||||
@Inject(ConfigService)
|
@Inject(ConfigService)
|
||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
|
@Inject(ConfigService)
|
||||||
|
|
||||||
|
private readonly adminService: AdminService,
|
||||||
|
private readonly reflector: Reflector,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async canActivate(context: ExecutionContext) {
|
async canActivate(context: ExecutionContext) {
|
||||||
@@ -28,21 +45,39 @@ export class AdminAuthGuard implements CanActivate {
|
|||||||
throw new UnauthorizedException();
|
throw new UnauthorizedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const payload = await this.jwtService
|
||||||
const payload = await this.jwtService.verifyAsync<IAdminTokenPayload>(token, {
|
.verifyAsync<IAdminTokenPayload>(token, {
|
||||||
secret,
|
secret,
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
this.logger.error('error in AdminAuthGuard', err);
|
||||||
|
throw new UnauthorizedException('Invalid or expired token');
|
||||||
});
|
});
|
||||||
request['adminId'] = payload.adminId;
|
request['adminId'] = payload.adminId;
|
||||||
request['restId'] = payload.restId;
|
request['restId'] = payload.restId;
|
||||||
} catch {
|
|
||||||
throw new UnauthorizedException();
|
// check if the user has the required permissions
|
||||||
|
const requiredPermissions =
|
||||||
|
this.reflector.getAllAndOverride<string[]>(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;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log('error in AuthGuard', err);
|
this.logger.error('error in AdminAuthGuard', err);
|
||||||
throw new UnauthorizedException('Invalid or expired token');
|
throw new UnauthorizedException('Invalid or expired token');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extractTokenFromHeader(request: Request): string | undefined {
|
private extractTokenFromHeader(request: Request): string | undefined {
|
||||||
const [type, token] = request.headers.authorization?.split(' ') ?? [];
|
const [type, token] = request.headers.authorization?.split(' ') ?? [];
|
||||||
return type === 'Bearer' ? token : undefined;
|
return type === 'Bearer' ? token : undefined;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { CanActivate, ExecutionContext, Injectable, UnauthorizedException, Injec
|
|||||||
import { Request } from 'express';
|
import { Request } from 'express';
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { IAdminTokenPayload, ITokenPayload } from '../interfaces/IToken-payload';
|
import { ITokenPayload } from '../interfaces/IToken-payload';
|
||||||
|
|
||||||
export interface AdminAuthRequest extends Request {
|
export interface AdminAuthRequest extends Request {
|
||||||
adminId: string;
|
adminId: string;
|
||||||
@@ -37,6 +37,7 @@ export class AuthGuard implements CanActivate {
|
|||||||
} catch {
|
} catch {
|
||||||
throw new UnauthorizedException();
|
throw new UnauthorizedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log('error in AuthGuard', err);
|
console.log('error in AuthGuard', err);
|
||||||
|
|||||||
@@ -3,34 +3,25 @@ import { Seeder } from '@mikro-orm/seeder';
|
|||||||
import { Category } from '../modules/foods/entities/category.entity';
|
import { Category } from '../modules/foods/entities/category.entity';
|
||||||
import { Food } from '../modules/foods/entities/food.entity';
|
import { Food } from '../modules/foods/entities/food.entity';
|
||||||
import { Admin } from '../modules/admin/entities/admin.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 { Role } from '../modules/admin/entities/role.entity';
|
||||||
import { Restaurant } from '../modules/restaurants/entities/restaurant.entity';
|
import { Restaurant } from '../modules/restaurants/entities/restaurant.entity';
|
||||||
import { User } from '../modules/users/entities/user.entity';
|
import { User } from '../modules/users/entities/user.entity';
|
||||||
|
import { Permission, PermissionTitles } from '../common/enums/permission.enum';
|
||||||
|
|
||||||
export class DatabaseSeeder extends Seeder {
|
export class DatabaseSeeder extends Seeder {
|
||||||
async run(em: EntityManager) {
|
async run(em: EntityManager) {
|
||||||
// 1. Create Permissions (English)
|
// 1. Create Permissions (English)
|
||||||
const permissions = [
|
const permissions = Object.values(Permission).map(name => ({
|
||||||
{ name: 'manage_foods', title: 'مدیریت غذاها' },
|
name,
|
||||||
{ name: 'manage_categories', title: 'مدیریت دستهبندیها' },
|
title: PermissionTitles[name],
|
||||||
{ 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 createdPermissions: Permission[] = [];
|
const createdPermissions: PermissionEntity[] = [];
|
||||||
for (const permData of permissions) {
|
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) {
|
if (!permission) {
|
||||||
permission = em.create(Permission, permData);
|
permission = em.create(PermissionEntity, permData);
|
||||||
em.persist(permission);
|
em.persist(permission);
|
||||||
}
|
}
|
||||||
createdPermissions.push(permission);
|
createdPermissions.push(permission);
|
||||||
@@ -51,7 +42,10 @@ export class DatabaseSeeder extends Seeder {
|
|||||||
const role = em.create(Role, { name: 'accountant', title: 'حسابدار' });
|
const role = em.create(Role, { name: 'accountant', title: 'حسابدار' });
|
||||||
// Add financial permissions to accountant
|
// Add financial permissions to accountant
|
||||||
const financialPerms = createdPermissions.filter(
|
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));
|
financialPerms.forEach(p => role.permissions.add(p));
|
||||||
em.persist(role);
|
em.persist(role);
|
||||||
@@ -62,7 +56,10 @@ export class DatabaseSeeder extends Seeder {
|
|||||||
const role = em.create(Role, { name: 'superAdmin', title: 'مدیر کل' });
|
const role = em.create(Role, { name: 'superAdmin', title: 'مدیر کل' });
|
||||||
// Add restaurant management permissions to superAdmin
|
// Add restaurant management permissions to superAdmin
|
||||||
const restaurantPerms = createdPermissions.filter(
|
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));
|
restaurantPerms.forEach(p => role.permissions.add(p));
|
||||||
em.persist(role);
|
em.persist(role);
|
||||||
|
|||||||
Reference in New Issue
Block a user