import type { DataAccessPolicy, DataAccessRule, DataAccessSection, DataVisibility, DataVisibilityScopes, SectionDataAccess, } from '@mento/shared'; import { DATA_ACCESS_LEGACY_SCOPE, DATA_ACCESS_SECTIONS, migrateVisibilityToSectionAccess, parseDataAccessPolicy, sectionHasDataAccessLimits, } from '@mento/shared'; import { canViewAllRecords } from '@mento/shared'; import type { Prisma } from '@prisma/client'; import type { Actor } from './record-scope'; function elevated(role: string | null | undefined): boolean { return canViewAllRecords(role); } export type SectionAccessContext = { section: SectionDataAccess; source: 'role' | 'override' | 'default'; }; function ownerIdList( rule: DataAccessRule, ctx: { userId: string; teamPeerIds: string[] | null }, ): string[] { const ownerIds = new Set(); if (!rule.owners?.length) return []; if (rule.owners.includes('self')) ownerIds.add(ctx.userId); if (rule.owners.includes('team')) { const peers = ctx.teamPeerIds?.length ? ctx.teamPeerIds : [ctx.userId]; for (const id of peers) ownerIds.add(id); } return [...ownerIds]; } /** Resolve یک بخش: override کاربر بر نقش؛ وگرنه مهاجرت از visibility قدیمی. */ export function resolveSectionAccess( sectionKey: DataAccessSection, input: { role: string; rolePolicy?: unknown; override?: unknown; legacyVisibility?: DataVisibility | string | null; legacyScopes?: DataVisibilityScopes | null; }, ): SectionAccessContext { if (elevated(input.role)) { return { section: { mode: 'all', rules: [] }, source: 'default' }; } const override = parseDataAccessPolicy(input.override); if (override[sectionKey]) { return { section: override[sectionKey]!, source: 'override' }; } const rolePolicy = parseDataAccessPolicy(input.rolePolicy); if (rolePolicy[sectionKey]) { return { section: rolePolicy[sectionKey]!, source: 'role' }; } // فرم محدودهٔ نقش، بخشِ ننوشته را «بدون محدودیت» نشان می‌دهد. // اگر نقش حتی یک بخش ذخیره کرده باشد، بقیه نباید به دید قدیمی own/team برگردند. if (Object.keys(rolePolicy).length > 0) { return { section: { mode: 'all', rules: [] }, source: 'role' }; } if (input.legacyVisibility != null || input.legacyScopes) { return { section: migrateVisibilityToSectionAccess( input.legacyVisibility, input.legacyScopes, DATA_ACCESS_LEGACY_SCOPE[sectionKey], ), source: 'default', }; } return { section: { mode: 'all', rules: [] }, source: 'default' }; } /** @deprecated */ export function resolveCustomersAccess(input: { role: string; rolePolicy?: unknown; override?: unknown; legacyVisibility?: DataVisibility | string | null; legacyScopes?: DataVisibilityScopes | null; }): SectionAccessContext { return resolveSectionAccess('customers', input); } export function resolveAllSectionAccess(input: { role: string; rolePolicy?: unknown; override?: unknown; legacyVisibility?: DataVisibility | string | null; legacyScopes?: DataVisibilityScopes | null; }): DataAccessPolicy { const out: DataAccessPolicy = {}; for (const key of DATA_ACCESS_SECTIONS) { out[key] = resolveSectionAccess(key, input).section; } return out; } export function buildContactRuleWhere( rule: DataAccessRule, ctx: { userId: string; teamPeerIds: string[] | null }, ): Prisma.PersonWhereInput | null { const and: Prisma.PersonWhereInput[] = []; const ids = ownerIdList(rule, ctx); if (rule.owners?.length) { if (!ids.length) return null; and.push(ids.length === 1 ? { ownerId: ids[0]! } : { ownerId: { in: ids } }); } if (rule.groupIds?.length) { and.push({ groupLinks: { some: { groupId: { in: rule.groupIds } } } }); } if (rule.sources?.length) { and.push({ OR: [{ importSource: { in: rule.sources } }, { sourceRef: { in: rule.sources } }], }); } if (rule.moods?.length) { and.push({ mood: { in: rule.moods } }); } if (!and.length) return null; return and.length === 1 ? and[0]! : { AND: and }; } function orParts(parts: T[]): T | { OR: T[] } | { id: string } { if (!parts.length) return { id: '__no_access__' } as T & { id: string }; return parts.length === 1 ? parts[0]! : { OR: parts }; } export function customersPersonWhere( actor: Pick, ): Prisma.PersonWhereInput { if (elevated(actor.role)) return {}; const section = actor.customersAccess ?? { mode: 'all' as const, rules: [] }; if (!sectionHasDataAccessLimits(section)) return {}; const parts: Prisma.PersonWhereInput[] = []; for (const rule of section.rules ?? []) { const w = buildContactRuleWhere(rule, { userId: actor.userId, teamPeerIds: actor.teamPeerIds ?? null, }); if (w) parts.push(w); } return orParts(parts); } export function customersCompanyWhere( actor: Pick, ): Prisma.CompanyWhereInput { if (elevated(actor.role)) return {}; const section = actor.customersAccess ?? { mode: 'all' as const, rules: [] }; if (!sectionHasDataAccessLimits(section)) return {}; const parts: Prisma.CompanyWhereInput[] = []; for (const rule of section.rules ?? []) { const and: Prisma.CompanyWhereInput[] = []; if (rule.owners?.length) { const ids = ownerIdList(rule, { userId: actor.userId, teamPeerIds: actor.teamPeerIds ?? null, }); if (!ids.length) continue; and.push(ids.length === 1 ? { ownerId: ids[0]! } : { ownerId: { in: ids } }); } if (rule.groupIds?.length) { and.push({ groupLinks: { some: { groupId: { in: rule.groupIds } } } }); } if (rule.sources?.length) { and.push({ OR: [{ importSource: { in: rule.sources } }, { sourceRef: { in: rule.sources } }], }); } if ( rule.moods?.length && !rule.owners?.length && !rule.groupIds?.length && !rule.sources?.length ) { continue; } if (!and.length) continue; parts.push(and.length === 1 ? and[0]! : { AND: and }); } return orParts(parts); } export function leadsDeepWhere( actor: Pick, ): Prisma.LeadWhereInput { if (elevated(actor.role)) return {}; const section = actor.leadsAccess ?? { mode: 'all' as const, rules: [] }; if (!sectionHasDataAccessLimits(section)) return {}; const parts: Prisma.LeadWhereInput[] = []; for (const rule of section.rules ?? []) { const and: Prisma.LeadWhereInput[] = []; if (rule.owners?.length) { const ids = ownerIdList(rule, { userId: actor.userId, teamPeerIds: actor.teamPeerIds ?? null, }); if (!ids.length) continue; and.push(ids.length === 1 ? { assigneeId: ids[0]! } : { assigneeId: { in: ids } }); } if (rule.stageIds?.length) { and.push({ stageId: { in: rule.stageIds } }); } if (rule.sources?.length) { and.push({ OR: rule.sources.map((s) => ({ source: { equals: s, mode: 'insensitive' as const }, })), }); } if (!and.length) continue; parts.push(and.length === 1 ? and[0]! : { AND: and }); } return orParts(parts); } export function dealsDeepWhere( actor: Pick, ): Prisma.DealWhereInput { if (elevated(actor.role)) return {}; const section = actor.salesAccess ?? { mode: 'all' as const, rules: [] }; if (!sectionHasDataAccessLimits(section)) return {}; const parts: Prisma.DealWhereInput[] = []; for (const rule of section.rules ?? []) { const and: Prisma.DealWhereInput[] = []; if (rule.owners?.length) { const ids = ownerIdList(rule, { userId: actor.userId, teamPeerIds: actor.teamPeerIds ?? null, }); if (!ids.length) continue; and.push({ OR: [ { ownerId: { in: ids } }, { assignees: { some: { id: { in: ids } } } }, ], }); } if (rule.pipelineIds?.length) { and.push({ pipelineId: { in: rule.pipelineIds } }); } if (rule.stageIds?.length) { and.push({ stageId: { in: rule.stageIds } }); } if (!and.length) continue; parts.push(and.length === 1 ? and[0]! : { AND: and }); } return orParts(parts); } export function activitiesDeepWhere( actor: Pick, ): Prisma.ActivityWhereInput { if (elevated(actor.role)) return {}; const section = actor.activitiesAccess ?? { mode: 'all' as const, rules: [] }; if (!sectionHasDataAccessLimits(section)) return {}; const parts: Prisma.ActivityWhereInput[] = []; for (const rule of section.rules ?? []) { const and: Prisma.ActivityWhereInput[] = []; if (rule.owners?.length) { const ids = ownerIdList(rule, { userId: actor.userId, teamPeerIds: actor.teamPeerIds ?? null, }); if (!ids.length) continue; and.push({ OR: [ { assigneeId: { in: ids } }, { assignees: { some: { id: { in: ids } } } }, { participantIds: { hasSome: ids } }, ], }); } if (rule.boardIds?.length) { and.push({ boardId: { in: rule.boardIds } }); } if (rule.stageIds?.length) { and.push({ stageId: { in: rule.stageIds } }); } if (rule.typeIds?.length) { and.push({ typeDefId: { in: rule.typeIds } }); } if (!and.length) continue; parts.push(and.length === 1 ? and[0]! : { AND: and }); } return orParts(parts); } export function notesDeepWhere( actor: Pick, ): Prisma.NoteWhereInput { const personalGate: Prisma.NoteWhereInput = { OR: [ { visibility: { not: 'personal' } }, { visibility: 'personal', authorId: actor.userId }, ], }; if (elevated(actor.role)) return personalGate; const section = actor.notesAccess ?? { mode: 'all' as const, rules: [] }; if (!sectionHasDataAccessLimits(section)) return personalGate; const parts: Prisma.NoteWhereInput[] = []; for (const rule of section.rules ?? []) { const and: Prisma.NoteWhereInput[] = []; if (rule.owners?.length) { const ids = ownerIdList(rule, { userId: actor.userId, teamPeerIds: actor.teamPeerIds ?? null, }); if (!ids.length) continue; and.push({ OR: [ { authorId: { in: ids } }, { deal: { OR: [ { ownerId: { in: ids } }, { assignees: { some: { id: { in: ids } } } }, ], }, }, { person: { ownerId: { in: ids } } }, { company: { ownerId: { in: ids } } }, ], }); } if (rule.sources?.length) { and.push({ noteType: { in: rule.sources } }); } if (!and.length) continue; parts.push(and.length === 1 ? and[0]! : { AND: and }); } return { AND: [personalGate, orParts(parts)], }; } export function ticketsDeepWhere( actor: Pick, ): Prisma.TicketWhereInput { if (elevated(actor.role)) return {}; const section = actor.ticketsAccess ?? { mode: 'all' as const, rules: [] }; if (!sectionHasDataAccessLimits(section)) return {}; const parts: Prisma.TicketWhereInput[] = []; for (const rule of section.rules ?? []) { const and: Prisma.TicketWhereInput[] = []; if (rule.owners?.length) { const ids = ownerIdList(rule, { userId: actor.userId, teamPeerIds: actor.teamPeerIds ?? null, }); if (!ids.length) continue; and.push({ OR: [{ assigneeId: { in: ids } }, { reporterId: { in: ids } }], }); } if (rule.sources?.length) { and.push({ status: { in: rule.sources as Array<'open' | 'in_progress' | 'waiting' | 'resolved' | 'closed'>, }, }); } if (!and.length) continue; parts.push(and.length === 1 ? and[0]! : { AND: and }); } return orParts(parts); } export function projectsDeepWhere( actor: Pick, ): Prisma.ProjectCardWhereInput { if (elevated(actor.role)) return {}; const section = actor.projectsAccess ?? { mode: 'all' as const, rules: [] }; if (!sectionHasDataAccessLimits(section)) return {}; const parts: Prisma.ProjectCardWhereInput[] = []; for (const rule of section.rules ?? []) { const and: Prisma.ProjectCardWhereInput[] = []; if (rule.owners?.length) { const ids = ownerIdList(rule, { userId: actor.userId, teamPeerIds: actor.teamPeerIds ?? null, }); if (!ids.length) continue; and.push({ OR: [ { ownerId: { in: ids } }, { assignees: { some: { id: { in: ids } } } }, ], }); } if (rule.boardIds?.length) { and.push({ boardId: { in: rule.boardIds } }); } if (rule.stageIds?.length) { and.push({ stageId: { in: rule.stageIds } }); } if (!and.length) continue; parts.push(and.length === 1 ? and[0]! : { AND: and }); } return orParts(parts); } export function routinesDeepWhere( actor: Pick, ): Prisma.RoutineTaskWhereInput { if (elevated(actor.role)) return {}; const section = actor.routinesAccess ?? { mode: 'all' as const, rules: [] }; if (!sectionHasDataAccessLimits(section)) return {}; const parts: Prisma.RoutineTaskWhereInput[] = []; for (const rule of section.rules ?? []) { const and: Prisma.RoutineTaskWhereInput[] = []; if (rule.owners?.length) { const ids = ownerIdList(rule, { userId: actor.userId, teamPeerIds: actor.teamPeerIds ?? null, }); if (!ids.length) continue; and.push(ids.length === 1 ? { assigneeId: ids[0]! } : { assigneeId: { in: ids } }); } if (rule.typeIds?.length) { and.push({ categoryId: { in: rule.typeIds } }); } if (!and.length) continue; parts.push(and.length === 1 ? and[0]! : { AND: and }); } return orParts(parts); } /** اتحاد شناسهٔ owners در قوانین؛ null = بدون محدودیت مالکی (یا mode=all) */ export function sectionOwnerIdUnion( section: SectionDataAccess | null | undefined, ctx: { userId: string; teamPeerIds: string[] | null }, ): string[] | null { if (!sectionHasDataAccessLimits(section)) return null; const ids = new Set(); let sawOwnerRule = false; let sawNonOwnerRule = false; for (const rule of section?.rules ?? []) { if (rule.owners?.length) { sawOwnerRule = true; for (const id of ownerIdList(rule, ctx)) ids.add(id); } else if (rule.groupIds?.length || rule.sources?.length || rule.moods?.length || rule.stageIds?.length || rule.pipelineIds?.length || rule.boardIds?.length || rule.typeIds?.length) { sawNonOwnerRule = true; } } if (sawNonOwnerRule && !sawOwnerRule) return null; if (!sawOwnerRule) return null; return [...ids]; } export function sectionNeedsTeamPeers(section: SectionDataAccess | null | undefined): boolean { if (!sectionHasDataAccessLimits(section)) return false; return (section?.rules ?? []).some((r) => r.owners?.includes('team')); } export function sanitizeDataAccessPolicyInput(raw: unknown): DataAccessPolicy { return parseDataAccessPolicy(raw); }