feat: enforce role section permissions and add a leads permission
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.
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user