They stayed on the board when a non-prior deal was attached, even though the person was prior. Co-authored-by: Cursor <cursoragent@cursor.com>
214 lines
6.9 KiB
TypeScript
214 lines
6.9 KiB
TypeScript
import type { DataScope, DataVisibility, DataVisibilityScopes, UserRole } from '@mento/shared';
|
|
import { DATA_SCOPES, canViewAllRecords } from '@mento/shared';
|
|
import type { Prisma } from '@prisma/client';
|
|
|
|
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<Record<DataScope, ScopeVisibility>>;
|
|
/** off = فیلتر خاموش؛ current = کار جاری؛ archive = بایگانی اطلاعات قبل */
|
|
priorLens?: 'off' | 'current' | 'archive';
|
|
/** لحظهٔ روشن شدن ماژول برای این محیط؛ فعالیتهای ساختهشده قبل از آن میتوانند قبلی باشند */
|
|
priorCutoff?: Date | null;
|
|
};
|
|
|
|
export type ScopedActor = Pick<Actor, 'userId' | 'role' | 'dataVisibility' | 'visibleUserIds' | 'scopes'>;
|
|
|
|
/** Actor با سطح دید مخصوص یک بخش (اگر تنظیم شده باشد). */
|
|
export function forScope<T extends ScopedActor>(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<string, unknown>)[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<Actor, 'role' | 'dataVisibility' | 'visibleUserIds' | 'scopes'>,
|
|
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: ScopedActor,
|
|
scope: DataScope = 'contacts',
|
|
): { ownerId: string } | { ownerId: { in: string[] } } | Record<string, never> {
|
|
const ids = scopedOwnerIds(actor, scope);
|
|
if (!ids) return {};
|
|
if (ids.length === 1) return { ownerId: ids[0]! };
|
|
return { ownerId: { in: ids } };
|
|
}
|
|
|
|
/** معامله: مالک اصلی یا یکی از مسئولان مشترک در محدوده دید */
|
|
export function dealAccessWhere(
|
|
actor: ScopedActor,
|
|
): Prisma.DealWhereInput {
|
|
const ids = scopedOwnerIds(actor, 'deals');
|
|
if (!ids) return {};
|
|
return {
|
|
OR: [
|
|
{ ownerId: { in: ids } },
|
|
{ assignees: { some: { id: { in: ids } } } },
|
|
],
|
|
};
|
|
}
|
|
|
|
/** کارت پروژه: مالک یا یکی از مسئولان در محدوده دید */
|
|
export function projectCardAccessWhere(
|
|
actor: ScopedActor,
|
|
): Prisma.ProjectCardWhereInput {
|
|
const ids = scopedOwnerIds(actor, 'projects');
|
|
if (!ids) return {};
|
|
return {
|
|
OR: [
|
|
{ ownerId: { in: ids } },
|
|
{ assignees: { some: { id: { in: ids } } } },
|
|
],
|
|
};
|
|
}
|
|
|
|
/** فعالیتها بر اساس مسئول اصلی یا مسئولان مشترک */
|
|
export function assigneeWhere(
|
|
actor: ScopedActor,
|
|
): Prisma.ActivityWhereInput {
|
|
const ids = scopedOwnerIds(actor, 'activities');
|
|
if (!ids) return {};
|
|
return {
|
|
OR: [
|
|
{ assigneeId: { in: ids } },
|
|
{ assignees: { some: { id: { in: ids } } } },
|
|
],
|
|
};
|
|
}
|
|
|
|
/** دسترسی به فعالیت: مسئولان یا شرکتکنندگان (فیلد شرکتکنندگان) در محدوده دید */
|
|
export function activityAccessWhere(
|
|
actor: ScopedActor,
|
|
): Prisma.ActivityWhereInput {
|
|
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,
|
|
): Prisma.NoteWhereInput {
|
|
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<Actor, 'userId' | 'role'>,
|
|
requested?: string | null,
|
|
): string {
|
|
if (elevated(actor.role) && requested?.trim()) {
|
|
return requested.trim();
|
|
}
|
|
return actor.userId;
|
|
}
|
|
|
|
/** تیکت: مسئول یا ثبتکننده در محدوده دید */
|
|
export function ticketAccessWhere(
|
|
actor: ScopedActor,
|
|
): Prisma.TicketWhereInput {
|
|
const ids = scopedOwnerIds(actor, 'tickets');
|
|
if (!ids) return {};
|
|
return {
|
|
OR: [{ assigneeId: { in: ids } }, { reporterId: { in: ids } }],
|
|
};
|
|
}
|
|
|
|
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';
|
|
}
|