fix(access): honor finance sections and team-member data scope
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>
This commit is contained in:
@@ -19,6 +19,10 @@ import type {
|
||||
StorageUsageDto,
|
||||
SubscriptionSummaryDto,
|
||||
} from '@mento/shared';
|
||||
import {
|
||||
parseUserTreasuryAccess,
|
||||
permissionsWithTreasuryOverride,
|
||||
} from '@mento/shared';
|
||||
import { Prisma } from '@prisma/client';
|
||||
/** Ensures Express.Multer.File augmentation is loaded for saveAvatar. */
|
||||
import type {} from 'multer';
|
||||
@@ -848,6 +852,9 @@ export class AuthService {
|
||||
},
|
||||
});
|
||||
const role = membership?.role ?? user.role;
|
||||
const rolePermissions = Array.isArray(membership?.orgRole?.permissions)
|
||||
? membership.orgRole.permissions.filter((item) => typeof item === 'string')
|
||||
: [];
|
||||
const orgName = membership?.organization.name;
|
||||
const sub =
|
||||
subscription ??
|
||||
@@ -879,9 +886,10 @@ export class AuthService {
|
||||
storage,
|
||||
isOrgOwner: role === 'owner',
|
||||
priorArchive: this.priorArchiveFor(membership),
|
||||
permissions: (Array.isArray(membership?.orgRole?.permissions)
|
||||
? membership.orgRole.permissions.filter((item) => typeof item === 'string')
|
||||
: []) as AuthUserDto['permissions'],
|
||||
permissions: permissionsWithTreasuryOverride(
|
||||
rolePermissions,
|
||||
parseUserTreasuryAccess(membership?.treasuryAccessOverride),
|
||||
) as AuthUserDto['permissions'],
|
||||
...(viewAs ? { viewAs } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -195,6 +195,10 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
const peers = await this.prisma.teamMember.findMany({
|
||||
where: {
|
||||
teamId: { in: memberships.map((m) => m.teamId) },
|
||||
team: { organizationId },
|
||||
user: {
|
||||
memberships: { some: { organizationId, isActive: true } },
|
||||
},
|
||||
},
|
||||
select: { userId: true },
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import type {
|
||||
SectionDataAccess,
|
||||
UserRole,
|
||||
} from '@mento/shared';
|
||||
import { DATA_SCOPES, canViewAllRecords } from '@mento/shared';
|
||||
import { DATA_SCOPES, canViewAllRecords, sectionHasDataAccessLimits } from '@mento/shared';
|
||||
import type { Prisma } from '@prisma/client';
|
||||
import {
|
||||
activitiesDeepWhere,
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
notesDeepWhere,
|
||||
projectsDeepWhere,
|
||||
routinesDeepWhere,
|
||||
sectionOwnerIdUnion,
|
||||
ticketsDeepWhere,
|
||||
} from './data-access';
|
||||
|
||||
@@ -60,6 +61,65 @@ export type Actor = {
|
||||
|
||||
export type ScopedActor = Pick<Actor, 'userId' | 'role' | 'dataVisibility' | 'visibleUserIds' | 'scopes'>;
|
||||
|
||||
type DeepAccessActor = ScopedActor &
|
||||
Partial<
|
||||
Pick<
|
||||
Actor,
|
||||
| 'customersAccess'
|
||||
| 'leadsAccess'
|
||||
| 'salesAccess'
|
||||
| 'activitiesAccess'
|
||||
| 'notesAccess'
|
||||
| 'routinesAccess'
|
||||
| 'projectsAccess'
|
||||
| 'ticketsAccess'
|
||||
| 'teamPeerIds'
|
||||
>
|
||||
>;
|
||||
|
||||
function sectionForScope(actor: DeepAccessActor, scope: DataScope): SectionDataAccess | undefined {
|
||||
switch (scope) {
|
||||
case 'contacts':
|
||||
return actor.customersAccess;
|
||||
case 'leads':
|
||||
return actor.leadsAccess;
|
||||
case 'deals':
|
||||
return actor.salesAccess;
|
||||
case 'activities':
|
||||
return actor.activitiesAccess;
|
||||
case 'notes':
|
||||
return actor.notesAccess;
|
||||
case 'routines':
|
||||
return actor.routinesAccess;
|
||||
case 'projects':
|
||||
return actor.projectsAccess;
|
||||
case 'tickets':
|
||||
return actor.ticketsAccess;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* شناسههای مالک از قانون عمق داده.
|
||||
* legacy = بخش عمیق روی این Actor نیست.
|
||||
* open = محدودیت مالکی ندارد (همه، یا فقط فیلترهای غیرمالک).
|
||||
*/
|
||||
function deepOwnerIds(
|
||||
actor: DeepAccessActor,
|
||||
scope: DataScope,
|
||||
): { kind: 'legacy' } | { kind: 'open' } | { kind: 'ids'; ids: string[] } {
|
||||
const section = sectionForScope(actor, scope);
|
||||
if (!section) return { kind: 'legacy' };
|
||||
if (!sectionHasDataAccessLimits(section)) return { kind: 'open' };
|
||||
const ids = sectionOwnerIdUnion(section, {
|
||||
userId: actor.userId,
|
||||
teamPeerIds: actor.teamPeerIds ?? null,
|
||||
});
|
||||
if (ids === null) return { kind: 'open' };
|
||||
return { kind: 'ids', ids };
|
||||
}
|
||||
|
||||
/** Actor با سطح دید مخصوص یک بخش (اگر تنظیم شده باشد). */
|
||||
export function forScope<T extends ScopedActor>(actor: T, scope: DataScope): T {
|
||||
const s = actor.scopes?.[scope];
|
||||
@@ -109,9 +169,16 @@ export function scopedOwnerIds(
|
||||
|
||||
/** فیلتر مالکیت؛ پیشفرض بخش «مشتریان» (Person / Company). برای فاکتور/پیشفاکتور scope=finance. */
|
||||
export function ownerWhere(
|
||||
actor: ScopedActor,
|
||||
actor: DeepAccessActor,
|
||||
scope: DataScope = 'contacts',
|
||||
): { ownerId: string } | { ownerId: { in: string[] } } | Record<string, never> {
|
||||
const deep = deepOwnerIds(actor, scope);
|
||||
if (deep.kind === 'open') return {};
|
||||
if (deep.kind === 'ids') {
|
||||
if (deep.ids.length === 1) return { ownerId: deep.ids[0]! };
|
||||
if (!deep.ids.length) return { ownerId: '__no_access__' };
|
||||
return { ownerId: { in: deep.ids } };
|
||||
}
|
||||
const ids = scopedOwnerIds(actor, scope);
|
||||
if (!ids) return {};
|
||||
if (ids.length === 1) return { ownerId: ids[0]! };
|
||||
@@ -153,11 +220,16 @@ export function projectCardAccessWhere(
|
||||
}
|
||||
|
||||
/** فعالیتها بر اساس مسئول اصلی یا مسئولان مشترک */
|
||||
export function assigneeWhere(
|
||||
actor: ScopedActor,
|
||||
): Prisma.ActivityWhereInput {
|
||||
const ids = scopedOwnerIds(actor, 'activities');
|
||||
export function assigneeWhere(actor: DeepAccessActor): Prisma.ActivityWhereInput {
|
||||
const deep = deepOwnerIds(actor, 'activities');
|
||||
const ids =
|
||||
deep.kind === 'open'
|
||||
? null
|
||||
: deep.kind === 'ids'
|
||||
? deep.ids
|
||||
: scopedOwnerIds(actor, 'activities');
|
||||
if (!ids) return {};
|
||||
if (!ids.length) return { assigneeId: '__no_access__' };
|
||||
return {
|
||||
OR: [
|
||||
{ assigneeId: { in: ids } },
|
||||
|
||||
@@ -112,9 +112,13 @@ export class CompaniesService {
|
||||
...(groupIds.length ? [{ groupLinks: { some: { groupId: { in: groupIds } } } }] : []),
|
||||
];
|
||||
const where: Prisma.CompanyWhereInput = {
|
||||
organizationId: actor.organizationId,
|
||||
...customersCompanyWhere(actor),
|
||||
...(and.length ? { AND: and } : {}),
|
||||
AND: [
|
||||
{
|
||||
organizationId: actor.organizationId,
|
||||
...customersCompanyWhere(actor),
|
||||
},
|
||||
...and,
|
||||
],
|
||||
};
|
||||
|
||||
const [rows, total] = await this.prisma.$transaction([
|
||||
|
||||
@@ -218,11 +218,15 @@ export class PeopleService implements OnModuleInit {
|
||||
if (disc) and.push(disc);
|
||||
if (groupIds.length) and.push({ groupLinks: { some: { groupId: { in: groupIds } } } });
|
||||
return {
|
||||
organizationId: actor.organizationId,
|
||||
...customersPersonWhere(actor),
|
||||
...(moods.length ? { mood: { in: moods } } : {}),
|
||||
...(sources.length ? { importSource: { in: sources } } : {}),
|
||||
...(and.length ? { AND: and } : {}),
|
||||
AND: [
|
||||
{
|
||||
organizationId: actor.organizationId,
|
||||
...customersPersonWhere(actor),
|
||||
},
|
||||
...(moods.length ? [{ mood: { in: moods } }] : []),
|
||||
...(sources.length ? [{ importSource: { in: sources } }] : []),
|
||||
...and,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -176,22 +176,26 @@ export class LeadsService {
|
||||
const createdRange = this.createdAtWhere(opts.createdFrom, opts.createdTo);
|
||||
if (createdRange) and.push(createdRange);
|
||||
return {
|
||||
organizationId: actor.organizationId,
|
||||
...leadAccessWhere(actor),
|
||||
archivedAt: opts.archived ? { not: null } : null,
|
||||
...(opts.converted ? { convertedPersonId: { not: null } } : {}),
|
||||
...(opts.stageId ? { stageId: opts.stageId } : {}),
|
||||
...(opts.assigneeId ? { assigneeId: opts.assigneeId } : {}),
|
||||
...(opts.unassigned ? { assigneeId: null } : {}),
|
||||
...(opts.city ? { city: { contains: opts.city, mode: 'insensitive' } } : {}),
|
||||
...(opts.region
|
||||
? { region: { contains: opts.region, mode: 'insensitive' } }
|
||||
: {}),
|
||||
...(opts.trade ? { trade: { contains: opts.trade, mode: 'insensitive' } } : {}),
|
||||
...(opts.source
|
||||
? { source: { contains: opts.source, mode: 'insensitive' } }
|
||||
: {}),
|
||||
...(and.length ? { AND: and } : {}),
|
||||
AND: [
|
||||
{
|
||||
organizationId: actor.organizationId,
|
||||
...leadAccessWhere(actor),
|
||||
archivedAt: opts.archived ? { not: null } : null,
|
||||
...(opts.converted ? { convertedPersonId: { not: null } } : {}),
|
||||
...(opts.stageId ? { stageId: opts.stageId } : {}),
|
||||
...(opts.city ? { city: { contains: opts.city, mode: 'insensitive' } } : {}),
|
||||
...(opts.region
|
||||
? { region: { contains: opts.region, mode: 'insensitive' } }
|
||||
: {}),
|
||||
...(opts.trade ? { trade: { contains: opts.trade, mode: 'insensitive' } } : {}),
|
||||
...(opts.source
|
||||
? { source: { contains: opts.source, mode: 'insensitive' } }
|
||||
: {}),
|
||||
},
|
||||
...(opts.unassigned ? [{ assigneeId: null }] : []),
|
||||
...(opts.assigneeId && !opts.unassigned ? [{ assigneeId: opts.assigneeId }] : []),
|
||||
...and,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1185,7 +1189,7 @@ export class LeadsService {
|
||||
newStage
|
||||
? this.prisma.lead.count({ where: { ...base, stageId: newStage.id } })
|
||||
: Promise.resolve(0),
|
||||
this.prisma.lead.count({ where: { ...base, assigneeId: null } }),
|
||||
this.prisma.lead.count({ where: { AND: [base, { assigneeId: null }] } }),
|
||||
convertedIds.length
|
||||
? this.prisma.lead.count({
|
||||
where: { ...base, stageId: { in: convertedIds } },
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from '@mento/shared';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { customersPersonWhere } from '../common/data-access';
|
||||
import {
|
||||
type Actor,
|
||||
assigneeWhere,
|
||||
@@ -54,7 +55,7 @@ export class ReportsService {
|
||||
const invoiceScope = ownerWhere(actor, 'finance');
|
||||
const activityScope = assigneeWhere(actor);
|
||||
const ticketScope = ticketAccessWhere(actor);
|
||||
const personScope = elevated(actor.role) ? {} : { ownerId: actor.userId };
|
||||
const personScope = customersPersonWhere(actor);
|
||||
|
||||
const durationMs = end.getTime() - start.getTime();
|
||||
const prevEnd = new Date(start.getTime() - 1);
|
||||
@@ -1534,7 +1535,7 @@ export class ReportsService {
|
||||
const orgId = actor.organizationId;
|
||||
const dealScope = ownerWhere(actor, 'deals');
|
||||
const activityScope = assigneeWhere(actor);
|
||||
const personScope = elevated(actor.role) ? {} : { ownerId: actor.userId };
|
||||
const personScope = customersPersonWhere(actor);
|
||||
const rates = await loadOrgRates(this.prisma, orgId);
|
||||
const now = new Date();
|
||||
const { jy, jm } = dateToJalali(now);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common';
|
||||
import {
|
||||
parseUserTreasuryAccess,
|
||||
permissionsWithTreasuryOverride,
|
||||
treasurySectionAllowed,
|
||||
type TreasurySectionKey,
|
||||
} from '@mento/shared';
|
||||
@@ -16,7 +17,12 @@ export class TreasuryAccessGuard implements CanActivate {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const req = context.switchToHttp().getRequest<{ user?: Actor; originalUrl?: string; url?: string }>();
|
||||
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;
|
||||
@@ -30,11 +36,18 @@ export class TreasuryAccessGuard implements CanActivate {
|
||||
const rolePermissions = Array.isArray(membership?.orgRole?.permissions)
|
||||
? membership.orgRole.permissions.map(String)
|
||||
: [];
|
||||
const permissions = permissionsForTreasury(
|
||||
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('به این بخش مدیریت مالی دسترسی ندارید.');
|
||||
}
|
||||
@@ -42,14 +55,11 @@ export class TreasuryAccessGuard implements CanActivate {
|
||||
}
|
||||
}
|
||||
|
||||
function permissionsForTreasury(
|
||||
rolePermissions: string[],
|
||||
override: ReturnType<typeof parseUserTreasuryAccess>,
|
||||
): string[] {
|
||||
if (!override) return rolePermissions;
|
||||
const rest = rolePermissions.filter((key) => key !== 'treasury' && !key.startsWith('treasury_'));
|
||||
if (!override.enabled) return rest;
|
||||
return ['treasury', ...override.sections, ...rest];
|
||||
/** فهرست حساب و دسته برای ثبت سند لازم است؛ صفحهٔ مدیریتشان جدا میماند. */
|
||||
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' {
|
||||
|
||||
@@ -21,6 +21,7 @@ import type {
|
||||
import { canManageSettings, canViewAllRecords } from '@/lib/roles';
|
||||
import {
|
||||
canOpenPath,
|
||||
firstAllowedFinancePath,
|
||||
hasSectionPermission,
|
||||
sectionPermissionForPath,
|
||||
} from '@/lib/section-permissions';
|
||||
@@ -204,12 +205,11 @@ function cardAllowedForNav(card: {
|
||||
}) {
|
||||
if (card.ownerOnly && auth.user?.role !== 'owner') return false;
|
||||
const perms = auth.user?.permissions ?? [];
|
||||
const canTreasury = auth.user?.role === 'owner'
|
||||
|| auth.user?.role === 'admin'
|
||||
|| perms.includes('treasury')
|
||||
|| perms.some((key) => key.startsWith('treasury_'));
|
||||
if (card.to?.startsWith('/finance')) {
|
||||
return moduleAllowed(card.module ?? 'treasury') && canTreasury;
|
||||
return (
|
||||
moduleAllowed(card.module ?? 'treasury')
|
||||
&& Boolean(firstAllowedFinancePath(auth.user?.role, perms))
|
||||
);
|
||||
}
|
||||
const section = card.to ? sectionPermissionForPath(card.to) : null;
|
||||
const hasSection = section
|
||||
@@ -246,7 +246,13 @@ const openHub = computed<NavHubDef | null>(() => {
|
||||
if (!openHubKey.value) return null;
|
||||
const hub = NAV_HUBS.find((item) => item.key === openHubKey.value);
|
||||
if (!hub) return null;
|
||||
const cards = hub.cards.filter((card) => cardAllowedForNav(card));
|
||||
const financeTo = firstAllowedFinancePath(auth.user?.role, auth.user?.permissions);
|
||||
const cards = hub.cards
|
||||
.filter((card) => cardAllowedForNav(card))
|
||||
.map((card) => {
|
||||
if (!financeTo || !card.to.startsWith('/finance') || card.to === financeTo) return card;
|
||||
return { ...card, to: financeTo };
|
||||
});
|
||||
if (
|
||||
hub.key === 'customers' &&
|
||||
isMobileNav.value &&
|
||||
|
||||
@@ -69,6 +69,30 @@ export function canOpenFinancePath(
|
||||
return treasurySectionAllowed(role, permissions, section);
|
||||
}
|
||||
|
||||
const FINANCE_ENTRIES: Array<{ path: string; section: TreasurySectionKey }> = [
|
||||
{ path: '/finance', section: 'treasury_dashboard' },
|
||||
{ path: '/finance/incomes', section: 'treasury_incomes' },
|
||||
{ path: '/finance/expenses', section: 'treasury_expenses' },
|
||||
{ path: '/finance/receivables', section: 'treasury_receivables' },
|
||||
{ path: '/finance/accounts', section: 'treasury_accounts' },
|
||||
{ path: '/finance/payroll', section: 'treasury_payroll' },
|
||||
{ path: '/finance/recurring', section: 'treasury_recurring' },
|
||||
{ path: '/finance/reports', section: 'treasury_reports' },
|
||||
{ path: '/finance/audit', section: 'treasury_audit' },
|
||||
{ path: '/finance/categories', section: 'treasury_categories' },
|
||||
];
|
||||
|
||||
/** اولین صفحهٔ مالی که این نقش واقعاً میتواند باز کند. */
|
||||
export function firstAllowedFinancePath(
|
||||
role: UserRole | string | null | undefined,
|
||||
permissions: readonly string[] | null | undefined,
|
||||
): string | null {
|
||||
for (const entry of FINANCE_ENTRIES) {
|
||||
if (treasurySectionAllowed(role, permissions, entry.section)) return entry.path;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function canOpenPath(
|
||||
role: UserRole | string | null | undefined,
|
||||
permissions: readonly string[] | null | undefined,
|
||||
|
||||
@@ -2,7 +2,12 @@ import { createRouter, createWebHistory } from 'vue-router';
|
||||
import type { UserRole } from '@mento/shared';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { usePlatformAuthStore } from '@/stores/platform-auth';
|
||||
import { canOpenFinancePath, canOpenPath, sectionPermissionForPath } from '@/lib/section-permissions';
|
||||
import {
|
||||
canOpenFinancePath,
|
||||
canOpenPath,
|
||||
firstAllowedFinancePath,
|
||||
sectionPermissionForPath,
|
||||
} from '@/lib/section-permissions';
|
||||
|
||||
export const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
@@ -683,13 +688,8 @@ router.beforeEach((to) => {
|
||||
if (canOpenFinancePath(auth.user?.role, auth.user?.permissions, String(to.path))) {
|
||||
return true;
|
||||
}
|
||||
const exactHome = to.path === '/finance' || to.path === '/finance/';
|
||||
if (
|
||||
exactHome &&
|
||||
canOpenFinancePath(auth.user?.role, auth.user?.permissions, '/finance/payroll')
|
||||
) {
|
||||
return { path: '/finance/payroll' };
|
||||
}
|
||||
const next = firstAllowedFinancePath(auth.user?.role, auth.user?.permissions);
|
||||
if (next && next !== to.path) return { path: next };
|
||||
}
|
||||
return { name: 'home' };
|
||||
}
|
||||
|
||||
@@ -214,6 +214,12 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
|
||||
if (token.value) void hydrate();
|
||||
|
||||
if (typeof document !== 'undefined') {
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible') void hydrate();
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
token,
|
||||
user,
|
||||
|
||||
@@ -96,8 +96,18 @@ function docLine(item: FinancialDocumentDto) {
|
||||
async function load() {
|
||||
const [docs, cats, acc, bals] = await Promise.all([
|
||||
api.get<FinancialDocumentDto[]>('/treasury/documents', { params: { kind, q: q.value || undefined } }),
|
||||
api.get<FinancialCategoryDto[]>('/treasury/categories', { params: { kind } }),
|
||||
api.get<FinancialAccountDto[]>('/treasury/accounts'),
|
||||
api.get<FinancialCategoryDto[]>('/treasury/categories', { params: { kind } }).catch((e: unknown) => {
|
||||
if (axios.isAxiosError(e) && (e.response?.status === 403 || e.response?.status === 404)) {
|
||||
return { data: [] as FinancialCategoryDto[] };
|
||||
}
|
||||
throw e;
|
||||
}),
|
||||
api.get<FinancialAccountDto[]>('/treasury/accounts').catch((e: unknown) => {
|
||||
if (axios.isAxiosError(e) && (e.response?.status === 403 || e.response?.status === 404)) {
|
||||
return { data: [] as FinancialAccountDto[] };
|
||||
}
|
||||
throw e;
|
||||
}),
|
||||
api.get<TreasuryPartyBalanceDto[]>('/treasury/party-balances', { params: { kind } }).catch((e: unknown) => {
|
||||
if (axios.isAxiosError(e) && e.response?.status === 404) return { data: [] as TreasuryPartyBalanceDto[] };
|
||||
throw e;
|
||||
|
||||
@@ -151,8 +151,18 @@ function docLine(item: FinancialDocumentDto) {
|
||||
async function load() {
|
||||
const [docs, cats, acc, dash, bals] = await Promise.all([
|
||||
api.get<FinancialDocumentDto[]>('/treasury/documents', { params: { kind, q: q.value || undefined } }),
|
||||
api.get<FinancialCategoryDto[]>('/treasury/categories', { params: { kind } }),
|
||||
api.get<FinancialAccountDto[]>('/treasury/accounts'),
|
||||
api.get<FinancialCategoryDto[]>('/treasury/categories', { params: { kind } }).catch((e: unknown) => {
|
||||
if (axios.isAxiosError(e) && (e.response?.status === 403 || e.response?.status === 404)) {
|
||||
return { data: [] as FinancialCategoryDto[] };
|
||||
}
|
||||
throw e;
|
||||
}),
|
||||
api.get<FinancialAccountDto[]>('/treasury/accounts').catch((e: unknown) => {
|
||||
if (axios.isAxiosError(e) && (e.response?.status === 403 || e.response?.status === 404)) {
|
||||
return { data: [] as FinancialAccountDto[] };
|
||||
}
|
||||
throw e;
|
||||
}),
|
||||
api.get<TreasuryDashboardDto>('/treasury/dashboard').catch(() => null),
|
||||
api.get<TreasuryPartyBalanceDto[]>('/treasury/party-balances', { params: { kind } }).catch((e: unknown) => {
|
||||
if (axios.isAxiosError(e) && e.response?.status === 404) return { data: [] as TreasuryPartyBalanceDto[] };
|
||||
|
||||
@@ -636,6 +636,22 @@ export function parseUserTreasuryAccess(raw: unknown): UserTreasuryAccess | null
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* دسترسی مالی کاربر، اگر تنظیم شده باشد، کلیدهای مالی نقش را جایگزین میکند.
|
||||
* null یعنی همان مجوزهای نقش.
|
||||
*/
|
||||
export function permissionsWithTreasuryOverride(
|
||||
rolePermissions: readonly string[],
|
||||
override: UserTreasuryAccess | null,
|
||||
): string[] {
|
||||
if (!override) return [...rolePermissions];
|
||||
const rest = rolePermissions.filter(
|
||||
(key) => key !== "treasury" && !key.startsWith("treasury_"),
|
||||
);
|
||||
if (!override.enabled) return rest;
|
||||
return ["treasury", ...override.sections, ...rest];
|
||||
}
|
||||
|
||||
export function emptySectionDataAccess(): SectionDataAccess {
|
||||
return { mode: "all", rules: [] };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user