feat(acl): deep data access by section for roles and users
Role default + user override with OR rules; covers customers, leads, sales, activities, notes, routines, projects, and tickets.
This commit is contained in:
@@ -112,3 +112,14 @@ export class UpdateUserVisibilityDto {
|
||||
@IsObject()
|
||||
scopes?: Record<string, 'own' | 'team' | 'org' | null>;
|
||||
}
|
||||
|
||||
export class UpdateUserDataAccessDto {
|
||||
/**
|
||||
* null = پاک کردن override و ارث از نقش.
|
||||
* آبجکت = جایگزینی کامل override.
|
||||
*/
|
||||
@ValidateIf((_, v) => v !== null)
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
dataAccessOverride?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
UpdateLogoutTimeDto,
|
||||
UpdateUserRoleDto,
|
||||
UpdateUserStatusDto,
|
||||
UpdateUserDataAccessDto,
|
||||
UpdateUserVisibilityDto,
|
||||
} from './dto';
|
||||
import { UsersService } from './users.service';
|
||||
@@ -143,6 +144,21 @@ export class UsersController {
|
||||
);
|
||||
}
|
||||
|
||||
@Patch(':id/data-access')
|
||||
@Roles('owner', 'admin')
|
||||
updateDataAccess(
|
||||
@CurrentUser() user: { organizationId: string; role: UserRole },
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateUserDataAccessDto,
|
||||
) {
|
||||
return this.usersService.updateDataAccess(
|
||||
user.organizationId,
|
||||
user.role,
|
||||
id,
|
||||
dto,
|
||||
);
|
||||
}
|
||||
|
||||
@Patch(':id/role')
|
||||
@Roles('owner', 'admin')
|
||||
updateRole(
|
||||
|
||||
@@ -14,7 +14,8 @@ import type {
|
||||
UpdateUserVisibilityRequest,
|
||||
UserRole,
|
||||
} from '@mento/shared';
|
||||
import { DATA_SCOPES, DATA_VISIBILITIES } from '@mento/shared';
|
||||
import { DATA_SCOPES, DATA_VISIBILITIES, parseDataAccessPolicy } from '@mento/shared';
|
||||
import { resolveAllSectionAccess } from '../common/data-access';
|
||||
import { parseVisibilityScopes } from '../common/record-scope';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import { Prisma } from '@prisma/client';
|
||||
@@ -44,11 +45,12 @@ type MembershipRow = {
|
||||
commissionPercent: { toString(): string } | number | null;
|
||||
dataVisibility?: string | null;
|
||||
dataVisibilityScopes?: unknown;
|
||||
dataAccessOverride?: unknown;
|
||||
priorArchiveEnabled?: boolean;
|
||||
logoutTime?: string | null;
|
||||
blockLoginAfterLogout?: boolean;
|
||||
createdAt: Date;
|
||||
orgRole: { id: string; name: string; color: string } | null;
|
||||
orgRole: { id: string; name: string; color: string; dataAccessPolicy?: unknown } | null;
|
||||
user: {
|
||||
id: string;
|
||||
email: string;
|
||||
@@ -360,6 +362,31 @@ export class UsersService {
|
||||
return this.dtoOf(organizationId, targetId);
|
||||
}
|
||||
|
||||
async updateDataAccess(
|
||||
organizationId: string,
|
||||
actorRole: UserRole,
|
||||
targetId: string,
|
||||
dto: { dataAccessOverride?: Record<string, unknown> | null },
|
||||
): Promise<OrgUserDto> {
|
||||
if (actorRole !== 'owner' && actorRole !== 'admin') {
|
||||
throw new ForbiddenException('اجازه تغییر محدوده داده را ندارید');
|
||||
}
|
||||
if (dto.dataAccessOverride === 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);
|
||||
await this.prisma.membership.update({
|
||||
where: { id: target.id },
|
||||
data: { dataAccessOverride: next },
|
||||
});
|
||||
return this.dtoOf(organizationId, targetId);
|
||||
}
|
||||
|
||||
async updateRole(
|
||||
organizationId: string,
|
||||
actor: { userId: string; role: UserRole },
|
||||
@@ -541,6 +568,23 @@ export class UsersService {
|
||||
row.role === 'owner' || row.role === 'admin'
|
||||
? {}
|
||||
: parseVisibilityScopes(row.dataVisibilityScopes),
|
||||
dataAccessOverride:
|
||||
row.role === 'owner' || row.role === 'admin'
|
||||
? {}
|
||||
: parseDataAccessPolicy(row.dataAccessOverride),
|
||||
...(() => {
|
||||
const effectiveDataAccess = resolveAllSectionAccess({
|
||||
role: row.role,
|
||||
rolePolicy: row.orgRole?.dataAccessPolicy,
|
||||
override: row.dataAccessOverride,
|
||||
legacyVisibility: row.dataVisibility,
|
||||
legacyScopes: parseVisibilityScopes(row.dataVisibilityScopes),
|
||||
});
|
||||
return {
|
||||
effectiveDataAccess,
|
||||
effectiveCustomersAccess: effectiveDataAccess.customers,
|
||||
};
|
||||
})(),
|
||||
priorArchiveEnabled: Boolean(row.priorArchiveEnabled),
|
||||
logoutTime: row.role === 'owner' ? null : row.logoutTime ?? null,
|
||||
blockLoginAfterLogout:
|
||||
|
||||
Reference in New Issue
Block a user