Unticking a section hides it from the menu, closes its routes and blocks its writes on the server; existing roles keep their current access via migration.
74 lines
2.8 KiB
TypeScript
74 lines
2.8 KiB
TypeScript
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<boolean> {
|
|
const meta = this.reflector.getAllAndOverride<SectionPermissionMeta | undefined>(
|
|
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;
|
|
}
|
|
}
|