import { CanActivate, ExecutionContext, ForbiddenException, Injectable, SetMetadata, } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import type { RolePermissionKey } from '@mento/shared'; import { ROLE_PERMISSION_ITEMS } from '@mento/shared'; import type { Actor } from '../common/record-scope'; import { PrismaService } from '../prisma/prisma.service'; const SECTION_PERMISSION_KEY = 'sectionPermission'; type SectionPermissionMeta = { key: RolePermissionKey; /** true: خواندن هم مجوز می‌خواهد. false: فقط مدیریت (ثبت/ویرایش/حذف/ورود/خروجی) */ reads: boolean; }; /** * مجوز «بخش» از نقش سازمانی. مالک و مدیر همیشه مجازند. * پیش‌فرض فقط مدیریت را می‌بندد تا انتخاب رکورد در فرم‌های بخش‌های دیگر کار کند. */ export const RequirePermission = (key: RolePermissionKey, opts?: { reads?: boolean }) => SetMetadata(SECTION_PERMISSION_KEY, { key, reads: opts?.reads ?? false } satisfies SectionPermissionMeta); function isReadRequest(method: string, url: string): boolean { if (method !== 'GET' && method !== 'HEAD') return false; const path = (url.split('?')[0] || '').replace(/\/+$/, ''); return !path.endsWith('/export'); } @Injectable() export class SectionPermissionGuard implements CanActivate { constructor( private readonly reflector: Reflector, private readonly prisma: PrismaService, ) {} async canActivate(context: ExecutionContext): Promise { const meta = this.reflector.getAllAndOverride( SECTION_PERMISSION_KEY, [context.getHandler(), context.getClass()], ); if (!meta) return true; const req = context.switchToHttp().getRequest<{ user?: Actor; method?: string; originalUrl?: string; url?: string; }>(); const actor = req.user; const label = ROLE_PERMISSION_ITEMS.find((item) => item.key === meta.key)?.label ?? meta.key; if (!actor) throw new ForbiddenException(`دسترسی به «${label}» مجاز نیست.`); if (actor.role === 'owner' || actor.role === 'admin') return true; const method = (req.method || 'GET').toUpperCase(); if (!meta.reads && isReadRequest(method, req.originalUrl || req.url || '')) return true; const membership = await this.prisma.membership.findFirst({ where: { userId: actor.userId, organizationId: actor.organizationId, isActive: true }, select: { orgRole: { select: { permissions: true } } }, }); const permissions = membership?.orgRole?.permissions ?? []; if (!permissions.includes(meta.key)) { throw new ForbiddenException(`دسترسی به «${label}» برای نقش شما مجاز نیست.`); } return true; } }