Role treasury keys now open the first allowed finance page, and team-member ownership limits lists, dashboard, and reports instead of the whole organization. Co-authored-by: Cursor <cursoragent@cursor.com>
85 lines
3.4 KiB
TypeScript
85 lines
3.4 KiB
TypeScript
import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common';
|
|
import {
|
|
parseUserTreasuryAccess,
|
|
permissionsWithTreasuryOverride,
|
|
treasurySectionAllowed,
|
|
type TreasurySectionKey,
|
|
} from '@mento/shared';
|
|
import type { Actor } from '../common/record-scope';
|
|
import { PrismaService } from '../prisma/prisma.service';
|
|
|
|
/**
|
|
* حقوق فقط با treasury_payroll.
|
|
* بقیه مسیرها با treasury باز میشوند؛ اگر بخش ریز انتخاب شده باشد فقط همانها.
|
|
*/
|
|
@Injectable()
|
|
export class TreasuryAccessGuard implements CanActivate {
|
|
constructor(private readonly prisma: PrismaService) {}
|
|
|
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
const req = context.switchToHttp().getRequest<{
|
|
user?: Actor;
|
|
method?: string;
|
|
originalUrl?: string;
|
|
url?: string;
|
|
}>();
|
|
const actor = req.user;
|
|
if (!actor) throw new ForbiddenException('دسترسی مالی مجاز نیست.');
|
|
if (actor.role === 'owner' || actor.role === 'admin') return true;
|
|
const membership = await this.prisma.membership.findFirst({
|
|
where: { userId: actor.userId, organizationId: actor.organizationId, isActive: true },
|
|
select: {
|
|
orgRole: { select: { permissions: true } },
|
|
treasuryAccessOverride: true,
|
|
},
|
|
});
|
|
const rolePermissions = Array.isArray(membership?.orgRole?.permissions)
|
|
? membership.orgRole.permissions.map(String)
|
|
: [];
|
|
const permissions = permissionsWithTreasuryOverride(
|
|
rolePermissions,
|
|
parseUserTreasuryAccess(membership?.treasuryAccessOverride),
|
|
);
|
|
const method = (req.method || 'GET').toUpperCase();
|
|
const url = req.originalUrl || req.url || '';
|
|
if (
|
|
isLookupForDocuments(method, url) &&
|
|
treasurySectionAllowed(actor.role, permissions, 'treasury_documents')
|
|
) {
|
|
return true;
|
|
}
|
|
if (!treasurySectionAllowed(actor.role, permissions, sectionForUrl(url))) {
|
|
throw new ForbiddenException('به این بخش مدیریت مالی دسترسی ندارید.');
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/** فهرست حساب و دسته برای ثبت سند لازم است؛ صفحهٔ مدیریتشان جدا میماند. */
|
|
function isLookupForDocuments(method: string, url: string): boolean {
|
|
if (method !== 'GET' && method !== 'HEAD') return false;
|
|
const path = (url.split('?')[0] || '').replace(/\/+$/, '');
|
|
return path.endsWith('/treasury/accounts') || path.endsWith('/treasury/categories');
|
|
}
|
|
|
|
function sectionForUrl(url: string): TreasurySectionKey | 'treasury_documents' {
|
|
if (url.includes('/treasury/payroll')) return 'treasury_payroll';
|
|
if (url.includes('/treasury/receivables') || url.includes('/treasury/installments')) {
|
|
return 'treasury_receivables';
|
|
}
|
|
if (url.includes('/treasury/recurring')) return 'treasury_recurring';
|
|
if (url.includes('/treasury/reports')) return 'treasury_reports';
|
|
if (url.includes('/treasury/audit')) return 'treasury_audit';
|
|
if (url.includes('/treasury/categories')) return 'treasury_categories';
|
|
if (
|
|
url.includes('/treasury/accounts') ||
|
|
url.includes('/treasury/transfers') ||
|
|
url.includes('/treasury/reconciliations') ||
|
|
url.includes('/treasury/adjustments')
|
|
) {
|
|
return 'treasury_accounts';
|
|
}
|
|
if (url.includes('/treasury/dashboard')) return 'treasury_dashboard';
|
|
return 'treasury_documents';
|
|
}
|