fix(treasury): scope finance access and hide it when the module is off

Accounts can be edited, including the opening balance, and finance sections and accounts can be granted on a role or on one user. The finance menu stays hidden when the organization module is disabled.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-11-04 10:02:31 +03:30
co-authored by Cursor
parent 14f4e9d7d8
commit 1ccbefa7a3
26 changed files with 895 additions and 135 deletions
+6
View File
@@ -128,4 +128,10 @@ export class UpdateUserDataAccessDto {
@IsOptional()
@IsObject()
dataAccessOverride?: Record<string, unknown> | null;
/** null = ارث از نقش */
@ValidateIf((_, v) => v !== null)
@IsOptional()
@IsObject()
treasuryAccessOverride?: Record<string, unknown> | null;
}
+28 -6
View File
@@ -14,7 +14,12 @@ import type {
UpdateUserVisibilityRequest,
UserRole,
} from '@mento/shared';
import { DATA_SCOPES, DATA_VISIBILITIES, parseDataAccessPolicy } from '@mento/shared';
import {
DATA_SCOPES,
DATA_VISIBILITIES,
parseDataAccessPolicy,
parseUserTreasuryAccess,
} from '@mento/shared';
import { resolveAllSectionAccess } from '../common/data-access';
import { parseVisibilityScopes } from '../common/record-scope';
import * as bcrypt from 'bcryptjs';
@@ -47,6 +52,7 @@ type MembershipRow = {
dataVisibility?: string | null;
dataVisibilityScopes?: unknown;
dataAccessOverride?: unknown;
treasuryAccessOverride?: unknown;
priorArchiveEnabled?: boolean;
logoutTime?: string | null;
blockLoginAfterLogout?: boolean;
@@ -370,23 +376,35 @@ export class UsersService {
organizationId: string,
actorRole: UserRole,
targetId: string,
dto: { dataAccessOverride?: Record<string, unknown> | null },
dto: {
dataAccessOverride?: Record<string, unknown> | null;
treasuryAccessOverride?: Record<string, unknown> | null;
},
): Promise<OrgUserDto> {
if (actorRole !== 'owner' && actorRole !== 'admin') {
throw new ForbiddenException('اجازه تغییر محدوده داده را ندارید');
}
if (dto.dataAccessOverride === undefined) {
if (dto.dataAccessOverride === undefined && dto.treasuryAccessOverride === undefined) {
throw new BadRequestException('تغییری ارسال نشده است');
}
const target = await this.memberships.findUserInOrgById(organizationId, targetId);
if (target.role === 'owner' || target.role === 'admin') {
throw new BadRequestException('مالک و مدیر محدودیت عمق داده ندارند');
}
const next =
dto.dataAccessOverride === null ? {} : parseDataAccessPolicy(dto.dataAccessOverride);
const data: Prisma.MembershipUpdateInput = {};
if (dto.dataAccessOverride !== undefined) {
data.dataAccessOverride =
dto.dataAccessOverride === null ? {} : parseDataAccessPolicy(dto.dataAccessOverride);
}
if (dto.treasuryAccessOverride !== undefined) {
data.treasuryAccessOverride =
dto.treasuryAccessOverride === null
? Prisma.DbNull
: (parseUserTreasuryAccess(dto.treasuryAccessOverride) ?? Prisma.DbNull);
}
await this.prisma.membership.update({
where: { id: target.id },
data: { dataAccessOverride: next },
data,
});
return this.dtoOf(organizationId, targetId);
}
@@ -606,6 +624,10 @@ export class UsersService {
row.role === 'owner' || row.role === 'admin'
? {}
: parseDataAccessPolicy(row.dataAccessOverride),
treasuryAccessOverride:
row.role === 'owner' || row.role === 'admin'
? null
: parseUserTreasuryAccess(row.treasuryAccessOverride),
...(() => {
const effectiveDataAccess = resolveAllSectionAccess({
role: row.role,