diff --git a/src/common/decorators/admin-perms.decorator.ts b/src/common/decorators/admin-perms.decorator.ts new file mode 100644 index 0000000..a7107ba --- /dev/null +++ b/src/common/decorators/admin-perms.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 AdminPerms = createParamDecorator((data: unknown, ctx: ExecutionContext): string[] => { + const request = ctx.switchToHttp().getRequest(); + return request.permissions || []; +}); diff --git a/src/modules/admin/controllers/admin.controller.ts b/src/modules/admin/controllers/admin.controller.ts index 3e5e40d..86ceab7 100644 --- a/src/modules/admin/controllers/admin.controller.ts +++ b/src/modules/admin/controllers/admin.controller.ts @@ -9,6 +9,7 @@ import { AdminId } from 'src/common/decorators/admin-id.decorator'; import { Admin } from '../entities/admin.entity'; import { PermissionEnum } from 'src/common/enums/permission.enum'; import { Permissions } from 'src/common/decorators/permissions.decorator'; +import { AdminPerms } from 'src/common/decorators/admin-perms.decorator'; @Controller() @ApiTags('admin') @@ -20,7 +21,7 @@ export class AdminController { @Post('admin/admin') @Permissions(PermissionEnum.MANAGE_ADMINS) @ApiOperation({ summary: 'Create a new admin' }) - async create(@Body() dto: CreateAdminDto,) { + async create(@Body() dto: CreateAdminDto) { const admin = await this.adminService.create(dto); return admin; } @@ -40,21 +41,21 @@ export class AdminController { @Get('admin/admins/me') @ApiOperation({ summary: 'Get current authenticated admin profile' }) - async getMe(@AdminId() adminId: string,) { - const admin = await this.adminService.findById(adminId,); + async getMe(@AdminId() adminId: string) { + const admin = await this.adminService.findById(adminId); return admin; } @Patch('admin/admins/me') @ApiOperation({ summary: 'Update current authenticated admin profile' }) - updateMe(@AdminId() adminId: string, @Body() dto: UpdateAdminMeDto,): Promise { + updateMe(@AdminId() adminId: string, @Body() dto: UpdateAdminMeDto): Promise { return this.adminService.updateMe(adminId, dto); } @Get('admin/menu/counts') @ApiOperation({ summary: 'Get metrics for admin panel' }) - getDashboardCounts(@AdminId() adminId: string) { - return this.adminService.getDashboardCounts(adminId); + getDashboardCounts(@AdminId() adminId: string, @AdminPerms() permissions: string[]) { + return this.adminService.getDashboardCounts(adminId, permissions); } @Get('admin/home/stats') @@ -71,26 +72,24 @@ export class AdminController { return this.adminService.getWeeklyOrders(); } - @Patch('admin/admins/:adminId') @Permissions(PermissionEnum.MANAGE_ADMINS) @ApiOperation({ summary: 'Update an admin' }) - update(@Param('adminId') adminId: string, @Body() dto: UpdateAdminDto,): Promise { + update(@Param('adminId') adminId: string, @Body() dto: UpdateAdminDto): Promise { return this.adminService.update(adminId, dto); } @Get('admin/admins/:adminId') @Permissions(PermissionEnum.MANAGE_ADMINS) @ApiOperation({ summary: 'Get an admin by ID' }) - getById(@Param('adminId') adminId: string,): Promise { - return this.adminService.findById(adminId,); + getById(@Param('adminId') adminId: string): Promise { + return this.adminService.findById(adminId); } @Delete('admin/admins/:adminId') @Permissions(PermissionEnum.MANAGE_ADMINS) @ApiOperation({ summary: 'Delete an admin by ID' }) - deleteById(@Param('adminId') adminId: string,): Promise { - return this.adminService.softDelete(adminId,); + deleteById(@Param('adminId') adminId: string): Promise { + return this.adminService.softDelete(adminId); } - } diff --git a/src/modules/admin/providers/admin.service.ts b/src/modules/admin/providers/admin.service.ts index 8163074..6a6d0d9 100644 --- a/src/modules/admin/providers/admin.service.ts +++ b/src/modules/admin/providers/admin.service.ts @@ -148,29 +148,10 @@ export class AdminService { } return admins } + - async ownedPermissions(adminId: string, permissionNames: PermissionEnum[]): Promise { - if (permissionNames.length === 0) return [] - - const admin = await this.adminRepository.findOne( - { id: adminId }, - { populate: ['role', 'role.permissions'] }, - ) - - if (!admin?.role?.permissions) return [] - - const adminPermissionNames = new Set( - admin.role.permissions.getItems().map((p) => p.name), - ) - return permissionNames.filter((name) => adminPermissionNames.has(name)) - } - - async getDashboardCounts(adminId: string) { - const permissions = await this.ownedPermissions(adminId, [ - PermissionEnum.VIEW_ORDERS, - PermissionEnum.VIEW_ASSIGNED_ORDERS, - ]); - + async getDashboardCounts(adminId: string, permissions: string[]) { + const canViewAnyOrder = permissions.includes(PermissionEnum.VIEW_ORDERS); const canViewAssignedOrder = permissions.includes(PermissionEnum.VIEW_ASSIGNED_ORDERS); diff --git a/src/modules/auth/guards/adminAuth.guard.ts b/src/modules/auth/guards/adminAuth.guard.ts index d27449d..5955b3a 100644 --- a/src/modules/auth/guards/adminAuth.guard.ts +++ b/src/modules/auth/guards/adminAuth.guard.ts @@ -17,6 +17,7 @@ import { AuthService } from '../services/auth.service'; export interface AdminAuthRequest extends Request { adminId: string; + permissions: string[]; } @Injectable() @@ -61,26 +62,24 @@ export class AdminAuthGuard implements CanActivate { request['adminId'] = payload.adminId; + const adminPermissions = await this.authService.getAdminPermissionsFromCache(payload.adminId); + request['permissions'] = adminPermissions; // 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; - } + if (requiredPermissions.length > 0) { + const hasPermission = requiredPermissions.every(p => adminPermissions.includes(p)); - const adminPermissions = await this.authService.getAdminPermissionsFromCache(payload.adminId) - - const hasPermission = requiredPermissions.every(p => adminPermissions.includes(p)); - - if (!hasPermission) { - this.logger.warn('Insufficient permissions', { - adminId: payload.adminId, - required: requiredPermissions, - has: hasPermission, - }); - throw new ForbiddenException('You are not authorized to access this resource'); + if (!hasPermission) { + this.logger.warn('Insufficient permissions', { + adminId: payload.adminId, + required: requiredPermissions, + has: hasPermission, + }); + throw new ForbiddenException('You are not authorized to access this resource'); + } } return true; diff --git a/src/modules/order/controllers/order.controller.ts b/src/modules/order/controllers/order.controller.ts index 62905e2..3da7f7c 100644 --- a/src/modules/order/controllers/order.controller.ts +++ b/src/modules/order/controllers/order.controller.ts @@ -7,6 +7,7 @@ import { FindOrdersDto } from '../dto/find-orders.dto'; import { CreateOrderAsAdminDto } from '../dto/create-order.dto'; import { UpdateOrderAsAdminDto } from '../dto/update-order.dto'; import { AdminId } from 'src/common/decorators/admin-id.decorator'; +import { AdminPerms } from 'src/common/decorators/admin-perms.decorator'; import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard'; import { Permissions } from 'src/common/decorators/permissions.decorator'; import { PermissionEnum } from 'src/common/enums/permission.enum'; @@ -39,7 +40,7 @@ export class OrderController { @Get('public/orders/stats') @UseGuards(AuthGuard) @ApiOperation({ summary: 'Get User Orders stats' }) - getStats( @UserId() userId: string) { + getStats(@UserId() userId: string) { return this.orderService.getStats(userId); } /*========================== Admin Routes =====================*/ @@ -55,17 +56,24 @@ export class OrderController { // Permission handled inside service @Get('admin/orders') @UseGuards(AdminAuthGuard) - // @Permissions(PermissionEnum.VIEW_ORDERS) @ApiOperation({ summary: 'Get all orders with pagination and filters' }) - findAllAsAdmin(@Query() dto: FindOrdersDto, @AdminId() adminId: string) { - return this.orderService.findOrdersAsAdmin(adminId, dto); + findAllAsAdmin( + @Query() dto: FindOrdersDto, + @AdminId() adminId: string, + @AdminPerms() permissions: string[], + ) { + return this.orderService.findOrdersAsAdmin(adminId, permissions, dto); } @Get('admin/orders/tab-counts') @UseGuards(AdminAuthGuard) @ApiOperation({ summary: 'Get order tab counts for admin order list' }) - getAdminOrderTabCounts(@Query() dto: FindOrdersDto, @AdminId() adminId: string) { - return this.orderService.getAdminOrderTabCounts(adminId, dto); + getAdminOrderTabCounts( + @Query() dto: FindOrdersDto, + @AdminId() adminId: string, + @AdminPerms() permissions: string[], + ) { + return this.orderService.getAdminOrderTabCounts(adminId, permissions, dto); } @Get('admin/orders/:id') @@ -106,8 +114,13 @@ export class OrderController { @Post('admin/orders/:orderId/status') @UseGuards(AdminAuthGuard) @ApiOperation({ summary: 'Assign Order Designer ' }) - updateStatus(@Param('orderId') orderId: string,@AdminId()adminId:string, @Body() dto: UpdateStatusDto) { - return this.orderService.updateStatus(orderId,adminId, dto); + updateStatus( + @Param('orderId') orderId: string, + @AdminId() adminId: string, + @AdminPerms() permissions: string[], + @Body() dto: UpdateStatusDto, + ) { + return this.orderService.updateStatus(orderId, adminId, permissions, dto); } diff --git a/src/modules/order/providers/order.service.ts b/src/modules/order/providers/order.service.ts index 3abc78d..19560c3 100644 --- a/src/modules/order/providers/order.service.ts +++ b/src/modules/order/providers/order.service.ts @@ -158,10 +158,11 @@ export class OrderService { }) } - async findOrdersAsAdmin(adminId: string, dto: FindOrdersDto): Promise> { - const permissions = await this.adminService.ownedPermissions(adminId, - [PermissionEnum.VIEW_ORDERS, PermissionEnum.VIEW_ASSIGNED_ORDERS]) - + async findOrdersAsAdmin( + adminId: string, + permissions: string[], + dto: FindOrdersDto, + ): Promise> { if (permissions.includes(PermissionEnum.VIEW_ORDERS)) { return this.orderRepository.findAllPaginated(dto) @@ -179,12 +180,11 @@ export class OrderService { } } - async getAdminOrderTabCounts(adminId: string, dto: FindOrdersDto): Promise { - const permissions = await this.adminService.ownedPermissions(adminId, [ - PermissionEnum.VIEW_ORDERS, - PermissionEnum.VIEW_ASSIGNED_ORDERS, - ]); - + async getAdminOrderTabCounts( + adminId: string, + permissions: string[], + dto: FindOrdersDto, + ): Promise { const canViewAnyOrder = permissions.includes(PermissionEnum.VIEW_ORDERS); const canViewAssignedOrder = permissions.includes(PermissionEnum.VIEW_ASSIGNED_ORDERS); @@ -300,18 +300,12 @@ export class OrderService { - async updateStatus(orderId: string, adminId: string, dto: UpdateStatusDto): Promise { - const statusPermissions = [ - PermissionEnum.CHANGE_ORDER_STATUS_FROM_ANY_TO_ANY, - PermissionEnum.CHANGE_ORDER_STATUS_TO_CANCELLED, - PermissionEnum.CHANGE_ORDER_STATUS_IN_PROGRESS, - PermissionEnum.CHANGE_ORDER_STATUS_FROM_IN_PROGRESS_TO_FINISHED, - PermissionEnum.CHANGE_ORDER_STATUS_FROM_FINISHED_TO_INVOICED, - ]; - - const permissions = await this.adminService.ownedPermissions(adminId, - [PermissionEnum.VIEW_ORDERS, PermissionEnum.VIEW_ASSIGNED_ORDERS, ...statusPermissions]); - + async updateStatus( + orderId: string, + adminId: string, + permissions: string[], + dto: UpdateStatusDto, + ): Promise { const canViewAnyOrder = permissions.includes(PermissionEnum.VIEW_ORDERS); const canViewAssignedOrder = permissions.includes(PermissionEnum.VIEW_ASSIGNED_ORDERS);