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:
@@ -0,0 +1 @@
|
||||
ALTER TABLE "OrgRole" ADD COLUMN "treasuryAccountIds" TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[];
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE "OrgRole" ADD COLUMN "treasuryOwnAccounts" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE "Membership" ADD COLUMN "treasuryAccessOverride" JSONB;
|
||||
@@ -353,6 +353,10 @@ model OrgRole {
|
||||
permissions String[] @default([])
|
||||
/// عمق داده بهازای بخش، مثلاً {"customers":{"mode":"rules","rules":[{"owners":["self"]}]}}
|
||||
dataAccessPolicy Json @default("{}")
|
||||
/// حسابهای مالی اضافی این نقش؛ همراه با حسابهایی که خودش مسئول است
|
||||
treasuryAccountIds String[] @default([])
|
||||
/// اعضای نقش، حسابهایی را ببینند که نام خودشان مسئول آن است
|
||||
treasuryOwnAccounts Boolean @default(false)
|
||||
/// owner | admin | sales | support — نقشهای سیستمی قابل حذف نیستند
|
||||
systemKey String?
|
||||
/// نقش والد برای سلسلهمراتب زیرنقشها
|
||||
@@ -387,6 +391,8 @@ model Membership {
|
||||
dataVisibilityScopes Json @default("{}")
|
||||
/// Override عمق داده نسبت به نقش؛ کلید غایب = ارث از OrgRole.dataAccessPolicy
|
||||
dataAccessOverride Json @default("{}")
|
||||
/// null = دسترسی مالی از نقش؛ وگرنه جایگزین بخشها و حسابهای مالی همین کاربر
|
||||
treasuryAccessOverride Json?
|
||||
/// سوئیچ «بایگانی اطلاعات قبل» برای این عضو؛ فقط وقتی ماژول محیط روشن است اثر دارد
|
||||
priorArchiveEnabled Boolean @default(false)
|
||||
/// current | archive
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Type } from 'class-transformer';
|
||||
import {
|
||||
ArrayUnique,
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsIn,
|
||||
IsObject,
|
||||
IsOptional,
|
||||
@@ -45,6 +46,16 @@ export class CreateOrgRoleDto {
|
||||
@ValidateIf((_, v) => v !== null)
|
||||
@IsString()
|
||||
parentId?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ArrayUnique()
|
||||
@IsString({ each: true })
|
||||
treasuryAccountIds?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
treasuryOwnAccounts?: boolean;
|
||||
}
|
||||
|
||||
export class UpdateOrgRoleDto {
|
||||
@@ -77,6 +88,16 @@ export class UpdateOrgRoleDto {
|
||||
@ValidateIf((_, v) => v !== null)
|
||||
@IsString()
|
||||
parentId?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ArrayUnique()
|
||||
@IsString({ each: true })
|
||||
treasuryAccountIds?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
treasuryOwnAccounts?: boolean;
|
||||
}
|
||||
|
||||
export class RolePermissionPatchItemDto {
|
||||
|
||||
@@ -32,6 +32,8 @@ type RoleRow = {
|
||||
color: string;
|
||||
permissions: string[];
|
||||
dataAccessPolicy?: unknown;
|
||||
treasuryAccountIds?: string[];
|
||||
treasuryOwnAccounts?: boolean;
|
||||
systemKey: string | null;
|
||||
parentId: string | null;
|
||||
createdAt: Date;
|
||||
@@ -79,6 +81,8 @@ export class OrgRolesService {
|
||||
permissions?: RolePermissionKey[];
|
||||
dataAccessPolicy?: DataAccessPolicy;
|
||||
parentId?: string | null;
|
||||
treasuryAccountIds?: string[];
|
||||
treasuryOwnAccounts?: boolean;
|
||||
},
|
||||
): Promise<OrgRoleDto> {
|
||||
this.assertManager(actorRole);
|
||||
@@ -108,6 +112,8 @@ export class OrgRolesService {
|
||||
description: (dto.description ?? '').trim(),
|
||||
color: dto.color ?? parent?.color ?? 'teal',
|
||||
permissions,
|
||||
treasuryAccountIds: dto.treasuryAccountIds ?? [],
|
||||
treasuryOwnAccounts: dto.treasuryOwnAccounts ?? false,
|
||||
parentId,
|
||||
...(dto.dataAccessPolicy
|
||||
? { dataAccessPolicy: parseDataAccessPolicy(dto.dataAccessPolicy) }
|
||||
@@ -135,6 +141,8 @@ export class OrgRolesService {
|
||||
permissions?: RolePermissionKey[];
|
||||
dataAccessPolicy?: DataAccessPolicy;
|
||||
parentId?: string | null;
|
||||
treasuryAccountIds?: string[];
|
||||
treasuryOwnAccounts?: boolean;
|
||||
},
|
||||
): Promise<OrgRoleDto> {
|
||||
this.assertManager(actorRole);
|
||||
@@ -159,6 +167,12 @@ export class OrgRolesService {
|
||||
if (dto.dataAccessPolicy !== undefined) {
|
||||
data.dataAccessPolicy = parseDataAccessPolicy(dto.dataAccessPolicy);
|
||||
}
|
||||
if (dto.treasuryAccountIds !== undefined) {
|
||||
data.treasuryAccountIds = [...new Set(dto.treasuryAccountIds.map((id) => id.trim()).filter(Boolean))];
|
||||
}
|
||||
if (dto.treasuryOwnAccounts !== undefined) {
|
||||
data.treasuryOwnAccounts = dto.treasuryOwnAccounts;
|
||||
}
|
||||
|
||||
if (dto.parentId !== undefined) {
|
||||
const nextParentId =
|
||||
@@ -426,6 +440,8 @@ export class OrgRolesService {
|
||||
color: row.color,
|
||||
permissions: sanitizePermissions(row.permissions),
|
||||
dataAccessPolicy: parseDataAccessPolicy(row.dataAccessPolicy),
|
||||
treasuryAccountIds: row.treasuryAccountIds ?? [],
|
||||
treasuryOwnAccounts: row.treasuryOwnAccounts ?? false,
|
||||
systemKey,
|
||||
parentId: row.parentId,
|
||||
parentName: row.parentId ? (nameById.get(row.parentId) ?? null) : null,
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import {
|
||||
parseUserTreasuryAccess,
|
||||
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) {}
|
||||
@@ -10,20 +19,56 @@ export class TreasuryAccessGuard implements CanActivate {
|
||||
const req = context.switchToHttp().getRequest<{ user?: Actor; originalUrl?: string; url?: string }>();
|
||||
const actor = req.user;
|
||||
if (!actor) throw new ForbiddenException('دسترسی مالی مجاز نیست.');
|
||||
const url = req.originalUrl || req.url || '';
|
||||
const payroll = url.includes('/treasury/payroll');
|
||||
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 } } },
|
||||
select: {
|
||||
orgRole: { select: { permissions: true } },
|
||||
treasuryAccessOverride: true,
|
||||
},
|
||||
});
|
||||
const permissions = Array.isArray(membership?.orgRole?.permissions)
|
||||
const rolePermissions = Array.isArray(membership?.orgRole?.permissions)
|
||||
? membership.orgRole.permissions.map(String)
|
||||
: [];
|
||||
const allowed = payroll
|
||||
? permissions.includes('treasury_payroll')
|
||||
: permissions.includes('treasury') || permissions.includes('treasury_payroll');
|
||||
if (!allowed) throw new ForbiddenException('دسترسی مالی برای نقش شما مجاز نیست.');
|
||||
const permissions = permissionsForTreasury(
|
||||
rolePermissions,
|
||||
parseUserTreasuryAccess(membership?.treasuryAccessOverride),
|
||||
);
|
||||
const url = req.originalUrl || req.url || '';
|
||||
if (!treasurySectionAllowed(actor.role, permissions, sectionForUrl(url))) {
|
||||
throw new ForbiddenException('به این بخش مدیریت مالی دسترسی ندارید.');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
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 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';
|
||||
}
|
||||
|
||||
@@ -61,7 +61,18 @@ export class TreasuryController {
|
||||
updateAccount(
|
||||
@CurrentUser() user: Actor,
|
||||
@Param('id') id: string,
|
||||
@Body() body: { name?: string; notes?: string | null; isActive?: boolean; responsibleName?: string | null },
|
||||
@Body()
|
||||
body: {
|
||||
name?: string;
|
||||
type?: 'bank' | 'cash' | 'petty';
|
||||
bankName?: string | null;
|
||||
accountRef?: string | null;
|
||||
notes?: string | null;
|
||||
isActive?: boolean;
|
||||
responsibleName?: string | null;
|
||||
openingBalance?: string | number;
|
||||
openingDate?: string;
|
||||
},
|
||||
) {
|
||||
return this.treasury.updateAccount(user, id, body);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { parseUserTreasuryAccess } from '@mento/shared';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import ExcelJS from 'exceljs';
|
||||
@@ -129,8 +131,12 @@ export class TreasuryService {
|
||||
actor: Actor,
|
||||
range?: { from?: string; to?: string },
|
||||
): Promise<FinancialAccountDto[]> {
|
||||
const scope = await this.accountScope(actor);
|
||||
const accounts = await this.prisma.financialAccount.findMany({
|
||||
where: { organizationId: actor.organizationId },
|
||||
where: {
|
||||
organizationId: actor.organizationId,
|
||||
...(scope ? { id: { in: scope } } : {}),
|
||||
},
|
||||
orderBy: [{ isActive: 'desc' }, { name: 'asc' }],
|
||||
});
|
||||
return Promise.all(accounts.map((account) => this.toAccountDto(account, range)));
|
||||
@@ -196,19 +202,89 @@ export class TreasuryService {
|
||||
async updateAccount(
|
||||
actor: Actor,
|
||||
id: string,
|
||||
input: { name?: string; notes?: string | null; isActive?: boolean; responsibleName?: string | null },
|
||||
input: {
|
||||
name?: string;
|
||||
type?: 'bank' | 'cash' | 'petty';
|
||||
bankName?: string | null;
|
||||
accountRef?: string | null;
|
||||
notes?: string | null;
|
||||
isActive?: boolean;
|
||||
responsibleName?: string | null;
|
||||
openingBalance?: string | number;
|
||||
openingDate?: string;
|
||||
},
|
||||
): Promise<FinancialAccountDto> {
|
||||
const existing = await this.requireAccount(actor.organizationId, id);
|
||||
const row = await this.prisma.financialAccount.update({
|
||||
where: { id: existing.id },
|
||||
data: {
|
||||
...(input.name?.trim() ? { name: input.name.trim() } : {}),
|
||||
...(input.notes !== undefined ? { notes: input.notes?.trim() || null } : {}),
|
||||
...(input.responsibleName !== undefined
|
||||
? { responsibleName: input.responsibleName?.trim() || null }
|
||||
: {}),
|
||||
...(input.isActive !== undefined ? { isActive: input.isActive } : {}),
|
||||
},
|
||||
const existing = await this.requireAccount(actor.organizationId, id, actor);
|
||||
if (input.type && !['bank', 'cash', 'petty'].includes(input.type)) {
|
||||
throw new BadRequestException('نوع حساب نامعتبر است.');
|
||||
}
|
||||
const opening = input.openingBalance !== undefined ? dec(input.openingBalance) : null;
|
||||
if (opening?.isNegative()) {
|
||||
throw new BadRequestException('موجودی اولیه نمیتواند منفی باشد.');
|
||||
}
|
||||
const openingDate =
|
||||
input.openingDate !== undefined
|
||||
? (startOfDay(input.openingDate) ?? existing.openingDate)
|
||||
: null;
|
||||
const row = await this.prisma.$transaction(async (tx) => {
|
||||
const updated = await tx.financialAccount.update({
|
||||
where: { id: existing.id },
|
||||
data: {
|
||||
...(input.name?.trim() ? { name: input.name.trim() } : {}),
|
||||
...(input.type ? { type: input.type } : {}),
|
||||
...(input.bankName !== undefined ? { bankName: input.bankName?.trim() || null } : {}),
|
||||
...(input.accountRef !== undefined ? { accountRef: input.accountRef?.trim() || null } : {}),
|
||||
...(input.notes !== undefined ? { notes: input.notes?.trim() || null } : {}),
|
||||
...(input.responsibleName !== undefined
|
||||
? { responsibleName: input.responsibleName?.trim() || null }
|
||||
: {}),
|
||||
...(input.isActive !== undefined ? { isActive: input.isActive } : {}),
|
||||
...(opening ? { openingBalance: opening } : {}),
|
||||
...(openingDate ? { openingDate } : {}),
|
||||
},
|
||||
});
|
||||
if (opening || openingDate) {
|
||||
const nextAmount = opening ?? existing.openingBalance;
|
||||
const nextDate = openingDate ?? existing.openingDate;
|
||||
const openings = await tx.financialMovement.findMany({
|
||||
where: { accountId: existing.id, kind: 'opening', voidedAt: null },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
});
|
||||
if (nextAmount.isZero()) {
|
||||
if (openings.length) {
|
||||
await tx.financialMovement.updateMany({
|
||||
where: { id: { in: openings.map((row) => row.id) } },
|
||||
data: { voidedAt: new Date() },
|
||||
});
|
||||
}
|
||||
} else if (openings[0]) {
|
||||
await tx.financialMovement.update({
|
||||
where: { id: openings[0].id },
|
||||
data: { amount: nextAmount, occurredAt: nextDate, direction: 'in' },
|
||||
});
|
||||
const extra = openings.slice(1).map((row) => row.id);
|
||||
if (extra.length) {
|
||||
await tx.financialMovement.updateMany({
|
||||
where: { id: { in: extra } },
|
||||
data: { voidedAt: new Date() },
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await tx.financialMovement.create({
|
||||
data: {
|
||||
organizationId: actor.organizationId,
|
||||
accountId: existing.id,
|
||||
direction: 'in',
|
||||
amount: nextAmount,
|
||||
kind: 'opening',
|
||||
occurredAt: nextDate,
|
||||
memo: 'مانده افتتاحیه',
|
||||
createdById: actor.userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
return this.toAccountDto(row);
|
||||
}
|
||||
@@ -218,7 +294,7 @@ export class TreasuryService {
|
||||
accountId: string,
|
||||
range?: { from?: string; to?: string },
|
||||
): Promise<{ account: FinancialAccountDto; items: FinancialMovementDto[] }> {
|
||||
const account = await this.requireAccount(actor.organizationId, accountId);
|
||||
const account = await this.requireAccount(actor.organizationId, accountId, actor);
|
||||
const from = startOfDay(range?.from);
|
||||
const to = startOfDay(range?.to);
|
||||
const rows = await this.prisma.financialMovement.findMany({
|
||||
@@ -303,8 +379,8 @@ export class TreasuryService {
|
||||
if (dup) throw new BadRequestException('این انتقال قبلاً ثبت شده است.');
|
||||
}
|
||||
const [source, target] = await Promise.all([
|
||||
this.requireAccount(actor.organizationId, input.fromAccountId),
|
||||
this.requireAccount(actor.organizationId, input.toAccountId),
|
||||
this.requireAccount(actor.organizationId, input.fromAccountId, actor),
|
||||
this.requireAccount(actor.organizationId, input.toAccountId, actor),
|
||||
]);
|
||||
if (!source.isActive || !target.isActive) {
|
||||
throw new BadRequestException('حساب غیرفعال در انتقال قابل استفاده نیست.');
|
||||
@@ -354,10 +430,12 @@ export class TreasuryService {
|
||||
kind: 'income' | 'expense',
|
||||
q?: string,
|
||||
): Promise<FinancialDocumentDto[]> {
|
||||
const scope = await this.accountScope(actor);
|
||||
const rows = await this.prisma.financialDocument.findMany({
|
||||
where: {
|
||||
organizationId: actor.organizationId,
|
||||
kind,
|
||||
...(scope ? { OR: [{ accountId: null }, { accountId: { in: scope } }] } : {}),
|
||||
...(q?.trim()
|
||||
? {
|
||||
OR: [
|
||||
@@ -408,7 +486,7 @@ export class TreasuryService {
|
||||
throw new BadRequestException('برای ثبت وجه قطعی، حساب را انتخاب کنید.');
|
||||
}
|
||||
if (input.accountId) {
|
||||
const account = await this.requireAccount(actor.organizationId, input.accountId);
|
||||
const account = await this.requireAccount(actor.organizationId, input.accountId, actor);
|
||||
if (!account.isActive) throw new BadRequestException('حساب غیرفعال است.');
|
||||
if (input.kind === 'expense' && settle.greaterThan(0)) {
|
||||
const balance = await this.accountBalance(account.id);
|
||||
@@ -482,7 +560,7 @@ export class TreasuryService {
|
||||
if (amount.greaterThan(remaining)) {
|
||||
throw new BadRequestException('مبلغ از مانده سند بیشتر است.');
|
||||
}
|
||||
const account = await this.requireAccount(actor.organizationId, input.accountId);
|
||||
const account = await this.requireAccount(actor.organizationId, input.accountId, actor);
|
||||
if (!account.isActive) throw new BadRequestException('حساب غیرفعال است.');
|
||||
if (doc.kind === 'expense') {
|
||||
const balance = await this.accountBalance(account.id);
|
||||
@@ -644,9 +722,14 @@ export class TreasuryService {
|
||||
): Promise<TreasuryDashboardDto> {
|
||||
const from = startOfDay(range?.from);
|
||||
const to = startOfDay(range?.to);
|
||||
const scope = await this.accountScope(actor);
|
||||
const accountFilter = scope
|
||||
? { OR: [{ accountId: null }, { accountId: { in: scope } }] }
|
||||
: {};
|
||||
const bookedWhere = {
|
||||
organizationId: actor.organizationId,
|
||||
status: { not: 'void' },
|
||||
...accountFilter,
|
||||
...(from || to
|
||||
? { bookedAt: { ...(from ? { gte: from } : {}), ...(to ? { lte: to } : {}) } }
|
||||
: {}),
|
||||
@@ -654,6 +737,7 @@ export class TreasuryService {
|
||||
const cashWhere = {
|
||||
organizationId: actor.organizationId,
|
||||
voidedAt: null,
|
||||
...(scope ? { accountId: { in: scope } } : {}),
|
||||
...(from || to
|
||||
? { occurredAt: { ...(from ? { gte: from } : {}), ...(to ? { lte: to } : {}) } }
|
||||
: {}),
|
||||
@@ -680,12 +764,13 @@ export class TreasuryService {
|
||||
where: {
|
||||
organizationId: actor.organizationId,
|
||||
status: { in: ['open', 'partial'] },
|
||||
...accountFilter,
|
||||
},
|
||||
include: { movements: true },
|
||||
}),
|
||||
this.listAccounts(actor),
|
||||
this.prisma.financialDocument.findMany({
|
||||
where: { organizationId: actor.organizationId },
|
||||
where: { organizationId: actor.organizationId, ...accountFilter },
|
||||
include: { category: true, account: true, movements: true },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 8,
|
||||
@@ -754,11 +839,50 @@ export class TreasuryService {
|
||||
);
|
||||
}
|
||||
|
||||
private async requireAccount(organizationId: string, id: string) {
|
||||
private async accountScope(actor: Actor): Promise<string[] | null> {
|
||||
if (actor.role === 'owner' || actor.role === 'admin') return null;
|
||||
const membership = await this.prisma.membership.findFirst({
|
||||
where: { userId: actor.userId, organizationId: actor.organizationId, isActive: true },
|
||||
select: {
|
||||
user: { select: { fullName: true } },
|
||||
orgRole: { select: { treasuryAccountIds: true, treasuryOwnAccounts: true } },
|
||||
treasuryAccessOverride: true,
|
||||
},
|
||||
});
|
||||
const override = parseUserTreasuryAccess(membership?.treasuryAccessOverride);
|
||||
if (override && !override.enabled) return [];
|
||||
const extra = override
|
||||
? override.accountIds
|
||||
: (membership?.orgRole?.treasuryAccountIds ?? []);
|
||||
const own = override
|
||||
? override.ownAccounts
|
||||
: Boolean(membership?.orgRole?.treasuryOwnAccounts);
|
||||
if (!own && extra.length === 0) return null;
|
||||
const ids = new Set(extra);
|
||||
const name = membership?.user.fullName?.trim();
|
||||
if (own && name) {
|
||||
const rows = await this.prisma.financialAccount.findMany({
|
||||
where: { organizationId: actor.organizationId, responsibleName: { not: null } },
|
||||
select: { id: true, responsibleName: true },
|
||||
});
|
||||
for (const row of rows) {
|
||||
if (row.responsibleName?.trim() === name) ids.add(row.id);
|
||||
}
|
||||
}
|
||||
return [...ids];
|
||||
}
|
||||
|
||||
private async requireAccount(organizationId: string, id: string, actor?: Actor) {
|
||||
const row = await this.prisma.financialAccount.findFirst({
|
||||
where: { id, organizationId },
|
||||
});
|
||||
if (!row) throw new NotFoundException('حساب پیدا نشد.');
|
||||
if (actor) {
|
||||
const scope = await this.accountScope(actor);
|
||||
if (scope && !scope.includes(row.id)) {
|
||||
throw new ForbiddenException('به این حساب دسترسی ندارید.');
|
||||
}
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user