import type { DataScope, DataVisibility, DataVisibilityScopes, SectionDataAccess, UserRole, } from '@mento/shared'; import { DATA_SCOPES, canViewAllRecords, sectionHasDataAccessLimits } from '@mento/shared'; import type { Prisma } from '@prisma/client'; import { activitiesDeepWhere, dealsDeepWhere, notesDeepWhere, projectsDeepWhere, routinesDeepWhere, sectionOwnerIdUnion, ticketsDeepWhere, } from './data-access'; export type ScopeVisibility = { dataVisibility: DataVisibility; visibleUserIds: string[] | null; }; export type Actor = { userId: string; role: string; organizationId: string; /** own | team | org — سطح پیش‌فرض از Membership */ dataVisibility?: DataVisibility; /** * شناسه کاربرانی که پرونده‌شان برای این Actor قابل‌مشاهده است. * اگر undefined باشد فقط خود کاربر؛ اگر null یعنی همه (elevated/org). */ visibleUserIds?: string[] | null; /** سطح دید تفکیکی هر بخش (از Membership.dataVisibilityScopes) */ scopes?: Partial>; /** عمق دادهٔ بخش‌ها (resolve شده از نقش + override) */ customersAccess?: SectionDataAccess; leadsAccess?: SectionDataAccess; salesAccess?: SectionDataAccess; activitiesAccess?: SectionDataAccess; notesAccess?: SectionDataAccess; routinesAccess?: SectionDataAccess; projectsAccess?: SectionDataAccess; ticketsAccess?: SectionDataAccess; /** هم‌تیمی‌ها برای قوانین owners:team (شامل خود کاربر) */ teamPeerIds?: string[] | null; /** off = فیلتر خاموش؛ current = کار جاری؛ archive = بایگانی اطلاعات قبل */ priorLens?: 'off' | 'current' | 'archive'; /** لحظهٔ روشن شدن ماژول برای این محیط؛ فعالیت‌های ساخته‌شده قبل از آن می‌توانند قبلی باشند */ priorCutoff?: Date | null; /** مالک واقعی وقتی در حالت مشاهده به‌جای کاربر دیگر است */ impersonatorUserId?: string; impersonatorRole?: string; /** شروع این نشست؛ برای ساعت خروج روزانه */ sessionStartedAt?: Date; /** در حالت مشاهده، سرور تغییر را رد می‌کند */ viewAsReadOnly?: boolean; }; export type ScopedActor = Pick; 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(actor: T, scope: DataScope): T { const s = actor.scopes?.[scope]; if (!s) return actor; return { ...actor, dataVisibility: s.dataVisibility, visibleUserIds: s.visibleUserIds }; } export function parseVisibilityScopes(raw: unknown): DataVisibilityScopes { const out: DataVisibilityScopes = {}; if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return out; for (const scope of DATA_SCOPES) { const v = (raw as Record)[scope]; if (v === 'own' || v === 'team' || v === 'org') out[scope] = v; } return out; } export function elevated(role: string | null | undefined): boolean { return canViewAllRecords(role); } export function seesAllRecords( actor: Pick, scope?: DataScope, ): boolean { if (elevated(actor.role)) return true; const s = scope ? actor.scopes?.[scope] : undefined; const visibility = s ? s.dataVisibility : actor.dataVisibility; const userIds = s ? s.visibleUserIds : actor.visibleUserIds; if (visibility === 'org') return true; if (userIds === null) return true; return false; } /** شناسه کاربرانی که پرونده‌شان در این بخش دیده می‌شود؛ null یعنی همه. */ export function scopedOwnerIds( actor: ScopedActor, scope?: DataScope, ): string[] | null { const a = scope ? forScope(actor, scope) : actor; if (seesAllRecords(a)) return null; const ids = a.visibleUserIds?.length ? a.visibleUserIds : [a.userId]; return [...new Set(ids.filter(Boolean))]; } /** فیلتر مالکیت؛ پیش‌فرض بخش «مشتریان» (Person / Company). برای فاکتور/پیش‌فاکتور scope=finance. */ export function ownerWhere( actor: DeepAccessActor, scope: DataScope = 'contacts', ): { ownerId: string } | { ownerId: { in: string[] } } | Record { 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]! }; return { ownerId: { in: ids } }; } /** معامله: عمق دادهٔ sales (JWT) یا مدل قدیمی own/team/org */ export function dealAccessWhere( actor: ScopedActor & Pick, ): Prisma.DealWhereInput { if (actor.salesAccess !== undefined) { return dealsDeepWhere(actor as Actor); } const ids = scopedOwnerIds(actor, 'deals'); if (!ids) return {}; return { OR: [ { ownerId: { in: ids } }, { assignees: { some: { id: { in: ids } } } }, ], }; } /** کارت پروژه: عمق داده یا مدل قدیمی own/team/org */ export function projectCardAccessWhere( actor: ScopedActor & Pick, ): Prisma.ProjectCardWhereInput { if (actor.projectsAccess !== undefined) { return projectsDeepWhere(actor as Actor); } const ids = scopedOwnerIds(actor, 'projects'); if (!ids) return {}; return { OR: [ { ownerId: { in: ids } }, { assignees: { some: { id: { in: ids } } } }, ], }; } /** فعالیت‌ها بر اساس مسئول اصلی یا مسئولان مشترک */ 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 } }, { assignees: { some: { id: { in: ids } } } }, ], }; } /** دسترسی به فعالیت: عمق دادهٔ activities (JWT) یا مدل قدیمی */ export function activityAccessWhere( actor: ScopedActor & Pick, ): Prisma.ActivityWhereInput { if (actor.activitiesAccess !== undefined) { return activitiesDeepWhere(actor as Actor); } const ids = scopedOwnerIds(actor, 'activities'); if (!ids) return {}; return { OR: [ { assigneeId: { in: ids } }, { assignees: { some: { id: { in: ids } } } }, { participantIds: { hasSome: ids } }, ], }; } /** یادداشت: عمق داده یا مدل قدیمی + محرمانگی شخصی */ export function noteAccessWhere( actor: ScopedActor & Pick, ): Prisma.NoteWhereInput { if (actor.notesAccess !== undefined) { return notesDeepWhere(actor as Actor); } const ids = scopedOwnerIds(actor, 'notes'); const personalGate: Prisma.NoteWhereInput = { OR: [ { visibility: { not: 'personal' } }, { visibility: 'personal', authorId: actor.userId }, // رکوردهای قدیمی بدون فیلد visibility در DB با default team می‌آیند ], }; if (!ids) return personalGate; return { AND: [ personalGate, { OR: [ { authorId: { in: ids } }, { deal: { OR: [ { ownerId: { in: ids } }, { assignees: { some: { id: { in: ids } } } }, ], }, }, { person: { ownerId: { in: ids } } }, { company: { ownerId: { in: ids } } }, ], }, ], }; } export function resolveOwnerId( actor: Pick, requested?: string | null, allowOthers?: boolean, ): string { const allowed = allowOthers ?? elevated(actor.role); if (allowed && requested?.trim()) { return requested.trim(); } return actor.userId; } /** تیکت: عمق داده یا مدل قدیمی */ export function ticketAccessWhere( actor: ScopedActor & Pick, ): Prisma.TicketWhereInput { if (actor.ticketsAccess !== undefined) { return ticketsDeepWhere(actor as Actor); } const ids = scopedOwnerIds(actor, 'tickets'); if (!ids) return {}; return { OR: [{ assigneeId: { in: ids } }, { reporterId: { in: ids } }], }; } /** کار روتین: عمق داده (وقتی JWT ست کرده) */ export function routineTaskAccessWhere( actor: Pick, ): Prisma.RoutineTaskWhereInput { if (actor.routinesAccess === undefined) return {}; return routinesDeepWhere(actor); } export function canReassignOwner(role: string | null | undefined): boolean { return elevated(role); } export type OrgRole = UserRole; export function normalizeDataVisibility( value: string | null | undefined, ): DataVisibility { if (value === 'team' || value === 'org' || value === 'own') return value; return 'own'; }