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:
@@ -374,6 +374,15 @@ export interface OrgUserDto {
|
||||
dataVisibility?: DataVisibility;
|
||||
/** سطح دسترسی تفکیکی هر بخش (فقط بخشهایی که با پیشفرض فرق دارند) */
|
||||
dataVisibilityScopes?: DataVisibilityScopes;
|
||||
/**
|
||||
* Override عمق داده نسبت به نقش؛ کلید غایب = ارث از نقش.
|
||||
* اگر کل آبجکت خالی باشد و نقش هم خالی باشد → بدون محدودیت عمقی.
|
||||
*/
|
||||
dataAccessOverride?: DataAccessPolicy;
|
||||
/** سیاست مؤثر پس از resolve (نقش + override) — فقط برای نمایش UI */
|
||||
effectiveDataAccess?: DataAccessPolicy;
|
||||
/** @deprecated از effectiveDataAccess.customers استفاده کنید */
|
||||
effectiveCustomersAccess?: SectionDataAccess;
|
||||
/** تیک مالک: این کاربر سوئیچ بایگانی اطلاعات قبل را ببیند */
|
||||
priorArchiveEnabled?: boolean;
|
||||
/** ساعت خروج روزانه HH:mm به وقت تهران؛ خالی یعنی بدون محدودیت */
|
||||
@@ -438,6 +447,226 @@ export interface UpdateUserVisibilityRequest {
|
||||
scopes?: Partial<Record<DataScope, DataVisibility | null>>;
|
||||
}
|
||||
|
||||
/* ——— Deep data access (customers + leads + sales + activities + notes/routines/projects/tickets) ——— */
|
||||
|
||||
export type DataAccessOwnerKind = "self" | "team";
|
||||
|
||||
export type DataAccessMode = "all" | "rules";
|
||||
|
||||
/**
|
||||
* بخشهای عمق داده.
|
||||
* customers/leads/sales/activities/notes ≈ RolePermissionKey؛
|
||||
* routines/projects/tickets ≈ ماژول / DataScope (بدون مجوز نقش جدا).
|
||||
*/
|
||||
export type DataAccessSection =
|
||||
| "customers"
|
||||
| "leads"
|
||||
| "sales"
|
||||
| "activities"
|
||||
| "notes"
|
||||
| "routines"
|
||||
| "projects"
|
||||
| "tickets";
|
||||
|
||||
export const DATA_ACCESS_SECTIONS: DataAccessSection[] = [
|
||||
"customers",
|
||||
"leads",
|
||||
"sales",
|
||||
"activities",
|
||||
"notes",
|
||||
"routines",
|
||||
"projects",
|
||||
"tickets",
|
||||
];
|
||||
|
||||
export const DATA_ACCESS_SECTION_LABELS: Record<DataAccessSection, string> = {
|
||||
customers: "مشتریان (اشخاص و شرکتها)",
|
||||
leads: "لیدها",
|
||||
sales: "فروش و معاملات",
|
||||
activities: "فعالیتها",
|
||||
notes: "یادداشتها",
|
||||
routines: "کارهای روتین",
|
||||
projects: "پروژهها",
|
||||
tickets: "تیکتها",
|
||||
};
|
||||
|
||||
/** نگاشت بخش عمقداده ← کلید قدیمی DataScope برای مهاجرت */
|
||||
export const DATA_ACCESS_LEGACY_SCOPE: Record<DataAccessSection, DataScope | null> = {
|
||||
customers: "contacts",
|
||||
leads: "leads",
|
||||
sales: "deals",
|
||||
activities: "activities",
|
||||
notes: "notes",
|
||||
routines: "routines",
|
||||
projects: "projects",
|
||||
tickets: "tickets",
|
||||
};
|
||||
|
||||
/**
|
||||
* یک قانون: فیلدهای پرشده با AND.
|
||||
* قوانین متعدد در SectionDataAccess با OR ترکیب میشوند.
|
||||
* ابعاد بلااستفاده برای یک بخش نادیده گرفته میشوند.
|
||||
*/
|
||||
export type DataAccessRule = {
|
||||
/** self / team → مالک / مسئول / نویسنده (بسته به بخش) */
|
||||
owners?: DataAccessOwnerKind[];
|
||||
/** مشتریان: گروهها */
|
||||
groupIds?: string[];
|
||||
/** مشتریان: importSource/sourceRef · لیدها: source · یادداشت: noteType · تیکت: status */
|
||||
sources?: string[];
|
||||
/** مشتریان: حال */
|
||||
moods?: PersonMood[];
|
||||
/** لید / معامله / فعالیت / پروژه: مرحله */
|
||||
stageIds?: string[];
|
||||
/** معامله: پایپلاین */
|
||||
pipelineIds?: string[];
|
||||
/** فعالیت / پروژه: بورد */
|
||||
boardIds?: string[];
|
||||
/** فعالیت: typeDefId · روتین: categoryId */
|
||||
typeIds?: string[];
|
||||
};
|
||||
|
||||
/** @deprecated نام قبلی؛ از DataAccessRule استفاده کنید */
|
||||
export type ContactAccessRule = DataAccessRule;
|
||||
|
||||
export type SectionDataAccess = {
|
||||
mode: DataAccessMode;
|
||||
rules?: DataAccessRule[];
|
||||
};
|
||||
|
||||
export type DataAccessPolicy = Partial<Record<DataAccessSection, SectionDataAccess>>;
|
||||
|
||||
export interface UpdateUserDataAccessRequest {
|
||||
/**
|
||||
* null یعنی حذف override و برگشت به ارث از نقش.
|
||||
* آبجکت یعنی جایگزینی کامل override (کلید غایب در آبجکت = آن بخش از override پاک میشود).
|
||||
*/
|
||||
dataAccessOverride?: DataAccessPolicy | null;
|
||||
}
|
||||
|
||||
export function emptySectionDataAccess(): SectionDataAccess {
|
||||
return { mode: "all", rules: [] };
|
||||
}
|
||||
|
||||
export function parseDataAccessPolicy(raw: unknown): DataAccessPolicy {
|
||||
const out: DataAccessPolicy = {};
|
||||
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return out;
|
||||
const obj = raw as Record<string, unknown>;
|
||||
for (const section of DATA_ACCESS_SECTIONS) {
|
||||
const parsed = parseSectionDataAccess(obj[section]);
|
||||
if (parsed) out[section] = parsed;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function parseSectionDataAccess(raw: unknown): SectionDataAccess | null {
|
||||
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
||||
const obj = raw as Record<string, unknown>;
|
||||
const mode: DataAccessMode = obj.mode === "rules" ? "rules" : "all";
|
||||
const rulesRaw = Array.isArray(obj.rules) ? obj.rules : [];
|
||||
const rules: DataAccessRule[] = [];
|
||||
for (const item of rulesRaw) {
|
||||
const rule = parseDataAccessRule(item);
|
||||
if (rule) rules.push(rule);
|
||||
}
|
||||
if (mode === "all") return { mode: "all", rules: [] };
|
||||
return { mode: "rules", rules };
|
||||
}
|
||||
|
||||
function parseStringIdList(raw: unknown): string[] | undefined {
|
||||
if (!Array.isArray(raw)) return undefined;
|
||||
const list = raw
|
||||
.filter((v): v is string => typeof v === "string" && v.trim().length > 0)
|
||||
.map((v) => v.trim());
|
||||
return list.length ? [...new Set(list)] : undefined;
|
||||
}
|
||||
|
||||
export function parseDataAccessRule(raw: unknown): DataAccessRule | null {
|
||||
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
||||
const obj = raw as Record<string, unknown>;
|
||||
const rule: DataAccessRule = {};
|
||||
if (Array.isArray(obj.owners)) {
|
||||
const owners = obj.owners.filter(
|
||||
(v): v is DataAccessOwnerKind => v === "self" || v === "team",
|
||||
);
|
||||
if (owners.length) rule.owners = [...new Set(owners)];
|
||||
}
|
||||
const groupIds = parseStringIdList(obj.groupIds);
|
||||
if (groupIds) rule.groupIds = groupIds;
|
||||
const sources = parseStringIdList(obj.sources);
|
||||
if (sources) rule.sources = sources;
|
||||
if (Array.isArray(obj.moods)) {
|
||||
const moods = obj.moods.filter(
|
||||
(v): v is PersonMood =>
|
||||
typeof v === "string" && (PERSON_MOODS as readonly string[]).includes(v),
|
||||
);
|
||||
if (moods.length) rule.moods = [...new Set(moods)];
|
||||
}
|
||||
const stageIds = parseStringIdList(obj.stageIds);
|
||||
if (stageIds) rule.stageIds = stageIds;
|
||||
const pipelineIds = parseStringIdList(obj.pipelineIds);
|
||||
if (pipelineIds) rule.pipelineIds = pipelineIds;
|
||||
const boardIds = parseStringIdList(obj.boardIds);
|
||||
if (boardIds) rule.boardIds = boardIds;
|
||||
const typeIds = parseStringIdList(obj.typeIds);
|
||||
if (typeIds) rule.typeIds = typeIds;
|
||||
|
||||
if (
|
||||
!rule.owners &&
|
||||
!rule.groupIds &&
|
||||
!rule.sources &&
|
||||
!rule.moods &&
|
||||
!rule.stageIds &&
|
||||
!rule.pipelineIds &&
|
||||
!rule.boardIds &&
|
||||
!rule.typeIds
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return rule;
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
export const parseContactAccessRule = parseDataAccessRule;
|
||||
|
||||
/** آیا این بخش محدودیت عمقی دارد؟ mode=all یا بدون قانون معتبر = بدون محدودیت */
|
||||
export function sectionHasDataAccessLimits(section: SectionDataAccess | null | undefined): boolean {
|
||||
if (!section || section.mode !== "rules") return false;
|
||||
return Boolean(section.rules?.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* مهاجرت از مدل قدیمی own/team/org به SectionDataAccess.
|
||||
* اگر scopeKey داده شود، سطح تفکیکی همان بخش اولویت دارد.
|
||||
*/
|
||||
export function migrateVisibilityToSectionAccess(
|
||||
dataVisibility: DataVisibility | string | null | undefined,
|
||||
scopes?: DataVisibilityScopes | null,
|
||||
scopeKey?: DataScope | null,
|
||||
): SectionDataAccess {
|
||||
const scoped =
|
||||
scopeKey && scopes
|
||||
? scopes[scopeKey]
|
||||
: undefined;
|
||||
const level =
|
||||
scoped === "own" || scoped === "team" || scoped === "org"
|
||||
? scoped
|
||||
: dataVisibility === "team" || dataVisibility === "org" || dataVisibility === "own"
|
||||
? dataVisibility
|
||||
: "own";
|
||||
if (level === "org") return { mode: "all", rules: [] };
|
||||
if (level === "team") return { mode: "rules", rules: [{ owners: ["self", "team"] }] };
|
||||
return { mode: "rules", rules: [{ owners: ["self"] }] };
|
||||
}
|
||||
|
||||
/** @deprecated از migrateVisibilityToSectionAccess استفاده کنید */
|
||||
export function migrateVisibilityToCustomersAccess(
|
||||
dataVisibility: DataVisibility | string | null | undefined,
|
||||
scopes?: DataVisibilityScopes | null,
|
||||
): SectionDataAccess {
|
||||
return migrateVisibilityToSectionAccess(dataVisibility, scopes, "contacts");
|
||||
}
|
||||
|
||||
export type RolePermissionKey =
|
||||
| "customers"
|
||||
| "leads"
|
||||
@@ -486,6 +715,8 @@ export interface OrgRoleDto {
|
||||
description: string;
|
||||
color: string;
|
||||
permissions: RolePermissionKey[];
|
||||
/** پیشفرض عمق داده برای اعضای این نقش */
|
||||
dataAccessPolicy?: DataAccessPolicy;
|
||||
systemKey: UserRole | null;
|
||||
/** نقش والد؛ null یعنی ریشه */
|
||||
parentId: string | null;
|
||||
@@ -504,6 +735,7 @@ export interface CreateOrgRoleRequest {
|
||||
description?: string;
|
||||
color?: string;
|
||||
permissions?: RolePermissionKey[];
|
||||
dataAccessPolicy?: DataAccessPolicy;
|
||||
/** اگر ست شود، این نقش زیرمجموعهٔ نقش والد خواهد بود */
|
||||
parentId?: string | null;
|
||||
}
|
||||
@@ -513,6 +745,7 @@ export interface UpdateOrgRoleRequest {
|
||||
description?: string;
|
||||
color?: string;
|
||||
permissions?: RolePermissionKey[];
|
||||
dataAccessPolicy?: DataAccessPolicy;
|
||||
/** null یعنی جدا کردن از والد و تبدیل به ریشه */
|
||||
parentId?: string | null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user