605 lines
22 KiB
TypeScript
605 lines
22 KiB
TypeScript
import { CatalogError, foundationErrors } from "@hivegrid/foundation";
|
|
import type { Prisma } from "@hivegrid/db/prisma-browser";
|
|
import type { TenantRlsClient } from "~/utils/db/.server/tenantRls.server";
|
|
import { TenantUserStatus } from "~/application/enums/tenants/TenantUserStatus";
|
|
import { projectReadScopeWhere, type ProjectVisibilityContext } from "./projects/projectService.server";
|
|
import type { FileEntityType } from "./fileService.server";
|
|
|
|
export type FileVisibilityContext = ProjectVisibilityContext & {
|
|
/** The user's permission names for this tenant (from getPermissionsByUser). */
|
|
permissions: readonly string[];
|
|
};
|
|
|
|
export type FileVisibilityQueryState = {
|
|
projectContext?: Promise<ProjectVisibilityContext>;
|
|
};
|
|
|
|
export function createFileVisibilityQueryState(): FileVisibilityQueryState {
|
|
return {};
|
|
}
|
|
|
|
const PROJECT_READ_PERMISSIONS = ["projects.read_all", "projects.read_own", "projects.read_own_pl", "projects.read_own_member"] as const;
|
|
const CRM_READ_PERMISSIONS = ["crm.read_all", "crm.read_own"] as const;
|
|
const LEAD_READ_PERMISSIONS = ["leads.read_all", "leads.read_own"] as const;
|
|
const TASK_READ_PERMISSIONS = ["tasks.read_all", "tasks.read_own"] as const;
|
|
const BOOKING_READ_PERMISSIONS = ["bookings.read_all", "bookings.read_own", "bookings.write", "bookings.admin"] as const;
|
|
const SIGNATURE_READ_PERMISSIONS = ["signature.read", "signature.send", "signature.admin"] as const;
|
|
const FILE_READ_PERMISSIONS = ["files.read_own", "files.read_all"] as const;
|
|
const CHAT_READ_PERMISSIONS = ["chat.read_own_channels", "chat.read_all_channels"] as const;
|
|
const HR_READ_ALL_PERMISSIONS = ["hr.read_all", "hr.admin"] as const;
|
|
const FILE_ENTITY_TYPES = new Set<FileEntityType>([
|
|
"contact",
|
|
"company",
|
|
"lead",
|
|
"deal",
|
|
"project",
|
|
"user",
|
|
"form_submission",
|
|
"order",
|
|
"task",
|
|
"appointment",
|
|
"signature_document",
|
|
"email_message",
|
|
"chat_message",
|
|
]);
|
|
|
|
function hasPermission(ctx: FileVisibilityContext, permission: string): boolean {
|
|
return ctx.permissions.includes(permission);
|
|
}
|
|
|
|
function hasAnyPermission(ctx: FileVisibilityContext, permissions: readonly string[]): boolean {
|
|
return permissions.some((permission) => hasPermission(ctx, permission));
|
|
}
|
|
|
|
function canReadAllHrFiles(ctx: FileVisibilityContext): boolean {
|
|
return hasAnyPermission(ctx, HR_READ_ALL_PERMISSIONS);
|
|
}
|
|
|
|
function targetNotFound(entityType: FileEntityType, entityId: string): CatalogError {
|
|
return new CatalogError(foundationErrors.notFound, { entityType, entityId });
|
|
}
|
|
|
|
async function hydrateProjectVisibilityContextWithTx(tx: TenantRlsClient, tenantId: string, ctx: FileVisibilityContext): Promise<ProjectVisibilityContext> {
|
|
const [roleIds, groupIds] = await Promise.all([
|
|
ctx.roleIds === undefined
|
|
? tx.userRole.findMany({ where: { tenantId, userId: ctx.userId }, select: { roleId: true } }).then((rows) => rows.map(({ roleId }) => roleId))
|
|
: Promise.resolve([...ctx.roleIds]),
|
|
ctx.groupIds === undefined
|
|
? tx.groupUser
|
|
.findMany({ where: { userId: ctx.userId, group: { tenantId } }, select: { groupId: true } })
|
|
.then((rows) => rows.map(({ groupId }) => groupId))
|
|
: Promise.resolve([...ctx.groupIds]),
|
|
]);
|
|
return { ...ctx, roleIds, groupIds };
|
|
}
|
|
|
|
function getProjectVisibilityContextWithTx(
|
|
tx: TenantRlsClient,
|
|
tenantId: string,
|
|
ctx: FileVisibilityContext,
|
|
queryState?: FileVisibilityQueryState
|
|
): Promise<ProjectVisibilityContext> {
|
|
if (!queryState) return hydrateProjectVisibilityContextWithTx(tx, tenantId, ctx);
|
|
queryState.projectContext ??= hydrateProjectVisibilityContextWithTx(tx, tenantId, ctx);
|
|
return queryState.projectContext;
|
|
}
|
|
|
|
type FileLinkTarget = {
|
|
entityType: string;
|
|
entityId: string;
|
|
};
|
|
|
|
function isFileEntityType(value: string): value is FileEntityType {
|
|
return FILE_ENTITY_TYPES.has(value as FileEntityType);
|
|
}
|
|
|
|
/**
|
|
* Filter an ordered set of polymorphic file links through the same target
|
|
* scopes used for mutations. Targets are grouped so each entity type needs at
|
|
* most one query; unsupported runtime types fail closed without a query.
|
|
*/
|
|
export async function filterVisibleFileLinksWithTx<T extends FileLinkTarget>(
|
|
tx: TenantRlsClient,
|
|
tenantId: string,
|
|
links: readonly T[],
|
|
ctx: FileVisibilityContext,
|
|
queryState?: FileVisibilityQueryState
|
|
): Promise<T[]> {
|
|
if (links.length === 0) return [];
|
|
|
|
const candidateIds = new Map<FileEntityType, Set<string>>();
|
|
for (const link of links) {
|
|
if (!isFileEntityType(link.entityType)) continue;
|
|
const ids = candidateIds.get(link.entityType) ?? new Set<string>();
|
|
ids.add(link.entityId);
|
|
candidateIds.set(link.entityType, ids);
|
|
}
|
|
|
|
const idsFor = (entityType: FileEntityType): string[] => [...(candidateIds.get(entityType) ?? [])];
|
|
const visibleIds = new Map<FileEntityType, Set<string>>();
|
|
const markVisible = (entityType: FileEntityType, ids: readonly string[]): void => {
|
|
if (ids.length === 0) return;
|
|
visibleIds.set(entityType, new Set(ids));
|
|
};
|
|
|
|
const directProjectIds = [...new Set([...idsFor("project"), ...idsFor("deal"), ...idsFor("order")])];
|
|
const signatureDocumentIds = idsFor("signature_document");
|
|
const chatMessageIds = idsFor("chat_message");
|
|
const canReadProjects = hasAnyPermission(ctx, PROJECT_READ_PERMISSIONS);
|
|
const canReadSignatures = hasAnyPermission(ctx, SIGNATURE_READ_PERMISSIONS);
|
|
const needsProjectContext = canReadProjects && (directProjectIds.length > 0 || (canReadSignatures && signatureDocumentIds.length > 0));
|
|
const projectContextPromise = needsProjectContext ? getProjectVisibilityContextWithTx(tx, tenantId, ctx, queryState) : undefined;
|
|
|
|
const independentQueries: Promise<void>[] = [];
|
|
|
|
if (chatMessageIds.length > 0 && hasAnyPermission(ctx, CHAT_READ_PERMISSIONS)) {
|
|
independentQueries.push(
|
|
tx.teamChatMessage
|
|
.findMany({
|
|
where: {
|
|
id: { in: chatMessageIds },
|
|
tenantId,
|
|
chat: { tenantId, members: { some: { tenantId, userId: ctx.userId, leftAt: null } } },
|
|
},
|
|
select: { id: true },
|
|
})
|
|
.then((rows) =>
|
|
markVisible(
|
|
"chat_message",
|
|
rows.map(({ id }) => id)
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
for (const [entityType, kontaktTyp] of [
|
|
["contact", "privatperson"],
|
|
["company", "organisation"],
|
|
] as const) {
|
|
const ids = idsFor(entityType);
|
|
if (ids.length === 0 || !hasAnyPermission(ctx, CRM_READ_PERMISSIONS)) continue;
|
|
independentQueries.push(
|
|
tx.kontakt
|
|
.findMany({
|
|
where: {
|
|
id: { in: ids },
|
|
tenantId,
|
|
archivedAt: null,
|
|
kontaktTyp,
|
|
...(hasPermission(ctx, "crm.read_all") ? {} : { zustaendigerUserId: ctx.userId }),
|
|
},
|
|
select: { id: true },
|
|
})
|
|
.then((rows) =>
|
|
markVisible(
|
|
entityType,
|
|
rows.map(({ id }) => id)
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
const leadIds = idsFor("lead");
|
|
if (leadIds.length > 0 && hasAnyPermission(ctx, LEAD_READ_PERMISSIONS)) {
|
|
independentQueries.push(
|
|
tx.inboxLead
|
|
.findMany({
|
|
where: {
|
|
id: { in: leadIds },
|
|
tenantId,
|
|
archivedAt: null,
|
|
...(hasPermission(ctx, "leads.read_all") ? {} : { zustaendigerUserId: ctx.userId }),
|
|
},
|
|
select: { id: true },
|
|
})
|
|
.then((rows) =>
|
|
markVisible(
|
|
"lead",
|
|
rows.map(({ id }) => id)
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
const userIds = idsFor("user");
|
|
const readableUserIds = canReadAllHrFiles(ctx) ? userIds : userIds.filter((userId) => userId === ctx.userId);
|
|
if (readableUserIds.length > 0) {
|
|
independentQueries.push(
|
|
tx.tenantUser
|
|
.findMany({
|
|
where: { tenantId, userId: { in: readableUserIds }, status: TenantUserStatus.ACTIVE, user: { active: true } },
|
|
select: { userId: true },
|
|
})
|
|
.then((rows) =>
|
|
markVisible(
|
|
"user",
|
|
rows.map(({ userId }) => userId)
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
const submissionIds = idsFor("form_submission");
|
|
if (submissionIds.length > 0 && (hasPermission(ctx, "forms.basic") || hasPermission(ctx, "forms.admin"))) {
|
|
independentQueries.push(
|
|
tx.formSubmission
|
|
.findMany({
|
|
where: {
|
|
id: { in: submissionIds },
|
|
tenantId,
|
|
form: { tenantId, ...(hasPermission(ctx, "forms.admin") ? {} : { createdByUserId: ctx.userId }) },
|
|
},
|
|
select: { id: true },
|
|
})
|
|
.then((rows) =>
|
|
markVisible(
|
|
"form_submission",
|
|
rows.map(({ id }) => id)
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
const taskIds = idsFor("task");
|
|
if (taskIds.length > 0 && hasAnyPermission(ctx, TASK_READ_PERMISSIONS)) {
|
|
const groupIdsPromise = hasPermission(ctx, "tasks.read_all")
|
|
? Promise.resolve([] as string[])
|
|
: ctx.groupIds !== undefined
|
|
? Promise.resolve([...ctx.groupIds])
|
|
: projectContextPromise
|
|
? projectContextPromise.then((projectCtx) => [...(projectCtx.groupIds ?? [])])
|
|
: tx.groupUser
|
|
.findMany({ where: { userId: ctx.userId, group: { tenantId } }, select: { groupId: true } })
|
|
.then((rows) => rows.map(({ groupId }) => groupId));
|
|
independentQueries.push(
|
|
groupIdsPromise.then(async (groupIds) => {
|
|
const rows = await tx.task.findMany({
|
|
where: {
|
|
id: { in: taskIds },
|
|
tenantId,
|
|
archivedAt: null,
|
|
...(hasPermission(ctx, "tasks.read_all")
|
|
? {}
|
|
: {
|
|
OR: [
|
|
{ createdByUserId: ctx.userId },
|
|
{ assignedToUserId: ctx.userId },
|
|
{ watchers: { some: { tenantId, userId: ctx.userId } } },
|
|
...(groupIds.length > 0 ? [{ assignedToGroupId: { in: groupIds } }] : []),
|
|
],
|
|
}),
|
|
},
|
|
select: { id: true },
|
|
});
|
|
markVisible(
|
|
"task",
|
|
rows.map(({ id }) => id)
|
|
);
|
|
})
|
|
);
|
|
}
|
|
|
|
const appointmentIds = idsFor("appointment");
|
|
if (appointmentIds.length > 0 && hasAnyPermission(ctx, BOOKING_READ_PERMISSIONS)) {
|
|
independentQueries.push(
|
|
tx.appointment
|
|
.findMany({
|
|
where: {
|
|
id: { in: appointmentIds },
|
|
tenantId,
|
|
...(hasPermission(ctx, "bookings.read_all") || hasPermission(ctx, "bookings.admin") ? {} : { providerUserId: ctx.userId }),
|
|
},
|
|
select: { id: true },
|
|
})
|
|
.then((rows) =>
|
|
markVisible(
|
|
"appointment",
|
|
rows.map(({ id }) => id)
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
const signatureDocumentsPromise =
|
|
signatureDocumentIds.length > 0 && canReadSignatures && canReadProjects
|
|
? tx.projektSignatureDocument.findMany({
|
|
where: { id: { in: signatureDocumentIds }, tenantId, archivedAt: null },
|
|
select: { id: true, projektId: true },
|
|
})
|
|
: Promise.resolve([] as Array<{ id: string; projektId: string }>);
|
|
|
|
const [, signatureDocuments] = await Promise.all([Promise.all(independentQueries), signatureDocumentsPromise]);
|
|
const projectIdsToCheck = [...new Set([...directProjectIds, ...signatureDocuments.map(({ projektId }) => projektId)])];
|
|
if (projectIdsToCheck.length > 0 && projectContextPromise) {
|
|
const projectCtx = await projectContextPromise;
|
|
const projects = await tx.projekt.findMany({
|
|
where: {
|
|
id: { in: projectIdsToCheck },
|
|
tenantId,
|
|
archivedAt: null,
|
|
...projectReadScopeWhere(tenantId, projectCtx),
|
|
},
|
|
select: { id: true },
|
|
});
|
|
const visibleProjectIds = new Set(projects.map(({ id }) => id));
|
|
for (const entityType of ["project", "deal", "order"] as const) {
|
|
markVisible(
|
|
entityType,
|
|
idsFor(entityType).filter((id) => visibleProjectIds.has(id))
|
|
);
|
|
}
|
|
markVisible(
|
|
"signature_document",
|
|
signatureDocuments.filter(({ projektId }) => visibleProjectIds.has(projektId)).map(({ id }) => id)
|
|
);
|
|
}
|
|
|
|
return links.filter((link) => isFileEntityType(link.entityType) && Boolean(visibleIds.get(link.entityType)?.has(link.entityId)));
|
|
}
|
|
|
|
/**
|
|
* Verify that a polymorphic FileEntityLink target both exists in this tenant and
|
|
* is readable by the current user. Unknown or not-yet-typed targets fail closed.
|
|
*/
|
|
export async function assertFileLinkTargetVisibleWithTx(
|
|
tx: TenantRlsClient,
|
|
tenantId: string,
|
|
entityType: FileEntityType,
|
|
entityId: string,
|
|
ctx: FileVisibilityContext
|
|
): Promise<void> {
|
|
let target: { id: string } | null = null;
|
|
|
|
switch (entityType) {
|
|
case "contact":
|
|
case "company":
|
|
if (!hasAnyPermission(ctx, CRM_READ_PERMISSIONS)) throw targetNotFound(entityType, entityId);
|
|
target = await tx.kontakt.findFirst({
|
|
where: {
|
|
id: entityId,
|
|
tenantId,
|
|
archivedAt: null,
|
|
kontaktTyp: entityType === "company" ? "organisation" : "privatperson",
|
|
...(hasPermission(ctx, "crm.read_all") ? {} : { zustaendigerUserId: ctx.userId }),
|
|
},
|
|
select: { id: true },
|
|
});
|
|
break;
|
|
case "lead":
|
|
if (!hasAnyPermission(ctx, LEAD_READ_PERMISSIONS)) throw targetNotFound(entityType, entityId);
|
|
target = await tx.inboxLead.findFirst({
|
|
where: {
|
|
id: entityId,
|
|
tenantId,
|
|
archivedAt: null,
|
|
...(hasPermission(ctx, "leads.read_all") ? {} : { zustaendigerUserId: ctx.userId }),
|
|
},
|
|
select: { id: true },
|
|
});
|
|
break;
|
|
case "project":
|
|
case "deal":
|
|
case "order": {
|
|
if (!hasAnyPermission(ctx, PROJECT_READ_PERMISSIONS)) throw targetNotFound(entityType, entityId);
|
|
const projectCtx = await hydrateProjectVisibilityContextWithTx(tx, tenantId, ctx);
|
|
target = await tx.projekt.findFirst({
|
|
where: { id: entityId, tenantId, archivedAt: null, ...projectReadScopeWhere(tenantId, projectCtx) },
|
|
select: { id: true },
|
|
});
|
|
break;
|
|
}
|
|
case "user":
|
|
if (entityId !== ctx.userId && !canReadAllHrFiles(ctx)) throw targetNotFound(entityType, entityId);
|
|
target = await tx.tenantUser.findFirst({
|
|
where: { tenantId, userId: entityId, status: TenantUserStatus.ACTIVE, user: { active: true } },
|
|
select: { id: true },
|
|
});
|
|
break;
|
|
case "form_submission":
|
|
if (!hasPermission(ctx, "forms.basic") && !hasPermission(ctx, "forms.admin")) throw targetNotFound(entityType, entityId);
|
|
target = await tx.formSubmission.findFirst({
|
|
where: {
|
|
id: entityId,
|
|
tenantId,
|
|
form: { tenantId, ...(hasPermission(ctx, "forms.admin") ? {} : { createdByUserId: ctx.userId }) },
|
|
},
|
|
select: { id: true },
|
|
});
|
|
break;
|
|
case "task": {
|
|
if (!hasAnyPermission(ctx, TASK_READ_PERMISSIONS)) throw targetNotFound(entityType, entityId);
|
|
const groupIds =
|
|
ctx.groupIds === undefined
|
|
? await tx.groupUser
|
|
.findMany({ where: { userId: ctx.userId, group: { tenantId } }, select: { groupId: true } })
|
|
.then((rows) => rows.map(({ groupId }) => groupId))
|
|
: [...ctx.groupIds];
|
|
target = await tx.task.findFirst({
|
|
where: {
|
|
id: entityId,
|
|
tenantId,
|
|
archivedAt: null,
|
|
...(hasPermission(ctx, "tasks.read_all")
|
|
? {}
|
|
: {
|
|
OR: [
|
|
{ createdByUserId: ctx.userId },
|
|
{ assignedToUserId: ctx.userId },
|
|
{ watchers: { some: { tenantId, userId: ctx.userId } } },
|
|
...(groupIds.length > 0 ? [{ assignedToGroupId: { in: groupIds } }] : []),
|
|
],
|
|
}),
|
|
},
|
|
select: { id: true },
|
|
});
|
|
break;
|
|
}
|
|
case "appointment":
|
|
if (!hasAnyPermission(ctx, BOOKING_READ_PERMISSIONS)) throw targetNotFound(entityType, entityId);
|
|
target = await tx.appointment.findFirst({
|
|
where: {
|
|
id: entityId,
|
|
tenantId,
|
|
...(hasPermission(ctx, "bookings.read_all") || hasPermission(ctx, "bookings.admin") ? {} : { providerUserId: ctx.userId }),
|
|
},
|
|
select: { id: true },
|
|
});
|
|
break;
|
|
case "signature_document": {
|
|
if (!hasAnyPermission(ctx, SIGNATURE_READ_PERMISSIONS)) throw targetNotFound(entityType, entityId);
|
|
if (!hasAnyPermission(ctx, PROJECT_READ_PERMISSIONS)) throw targetNotFound(entityType, entityId);
|
|
const projectCtx = await hydrateProjectVisibilityContextWithTx(tx, tenantId, ctx);
|
|
const document = await tx.projektSignatureDocument.findFirst({
|
|
where: { id: entityId, tenantId, archivedAt: null },
|
|
select: { id: true, projektId: true },
|
|
});
|
|
if (document) {
|
|
const project = await tx.projekt.findFirst({
|
|
where: { id: document.projektId, tenantId, archivedAt: null, ...projectReadScopeWhere(tenantId, projectCtx) },
|
|
select: { id: true },
|
|
});
|
|
target = project ? { id: document.id } : null;
|
|
}
|
|
break;
|
|
}
|
|
case "chat_message":
|
|
if (!hasAnyPermission(ctx, CHAT_READ_PERMISSIONS)) throw targetNotFound(entityType, entityId);
|
|
target = await tx.teamChatMessage.findFirst({
|
|
where: {
|
|
id: entityId,
|
|
tenantId,
|
|
chat: { tenantId, members: { some: { tenantId, userId: ctx.userId, leftAt: null } } },
|
|
},
|
|
select: { id: true },
|
|
});
|
|
break;
|
|
default:
|
|
throw targetNotFound(entityType, entityId);
|
|
}
|
|
|
|
if (!target) throw targetNotFound(entityType, entityId);
|
|
}
|
|
|
|
async function loadVisibleLinkedProjectIdsWithTx(
|
|
tx: TenantRlsClient,
|
|
tenantId: string,
|
|
ctx: FileVisibilityContext,
|
|
candidateFileIds?: readonly string[],
|
|
queryState?: FileVisibilityQueryState
|
|
): Promise<string[]> {
|
|
if (!PROJECT_READ_PERMISSIONS.some((permission) => hasPermission(ctx, permission))) return [];
|
|
let projectIds: string[] | undefined;
|
|
if (candidateFileIds !== undefined) {
|
|
if (candidateFileIds.length === 0) return [];
|
|
const links = await tx.fileEntityLink.findMany({
|
|
where: {
|
|
tenantId,
|
|
entityType: "project",
|
|
fileId: { in: [...candidateFileIds] },
|
|
},
|
|
select: { entityId: true },
|
|
distinct: ["entityId"],
|
|
});
|
|
projectIds = [...new Set(links.map(({ entityId }) => entityId))];
|
|
if (projectIds.length === 0) return [];
|
|
}
|
|
const projectCtx = await getProjectVisibilityContextWithTx(tx, tenantId, ctx, queryState);
|
|
const visibleProjects = await tx.projekt.findMany({
|
|
where: {
|
|
tenantId,
|
|
...(projectIds ? { id: { in: projectIds } } : {}),
|
|
archivedAt: null,
|
|
...projectReadScopeWhere(tenantId, projectCtx),
|
|
},
|
|
select: { id: true },
|
|
});
|
|
return visibleProjects.map(({ id }) => id);
|
|
}
|
|
|
|
async function loadVisibleLinkedChatMessageIdsWithTx(
|
|
tx: TenantRlsClient,
|
|
tenantId: string,
|
|
ctx: FileVisibilityContext,
|
|
candidateFileIds?: readonly string[]
|
|
): Promise<string[]> {
|
|
if (!hasAnyPermission(ctx, CHAT_READ_PERMISSIONS)) return [];
|
|
let messageIds: string[] | undefined;
|
|
if (candidateFileIds !== undefined) {
|
|
if (candidateFileIds.length === 0) return [];
|
|
const links = await tx.fileEntityLink.findMany({
|
|
where: { tenantId, entityType: "chat_message", fileId: { in: [...candidateFileIds] } },
|
|
select: { entityId: true },
|
|
distinct: ["entityId"],
|
|
});
|
|
messageIds = [...new Set(links.map(({ entityId }) => entityId))];
|
|
if (messageIds.length === 0) return [];
|
|
}
|
|
const messages = await tx.teamChatMessage.findMany({
|
|
where: {
|
|
tenantId,
|
|
...(messageIds ? { id: { in: messageIds } } : {}),
|
|
chat: { tenantId, members: { some: { tenantId, userId: ctx.userId, leftAt: null } } },
|
|
},
|
|
select: { id: true },
|
|
});
|
|
return messages.map(({ id }) => id);
|
|
}
|
|
|
|
/**
|
|
* Build the authoritative file read scope. A polymorphic link never grants
|
|
* access by itself: only project links whose target project is currently
|
|
* readable (including PipelineAccess deny rules) extend files.read_own.
|
|
*/
|
|
export async function fileReadScopeWhereWithTx(
|
|
tx: TenantRlsClient,
|
|
tenantId: string,
|
|
ctx: FileVisibilityContext,
|
|
candidateFileIds?: readonly string[],
|
|
queryState?: FileVisibilityQueryState
|
|
): Promise<Prisma.FileWhereInput> {
|
|
const canReadFiles = hasAnyPermission(ctx, FILE_READ_PERMISSIONS);
|
|
const canReadChat = hasAnyPermission(ctx, CHAT_READ_PERMISSIONS);
|
|
if (!canReadFiles && !canReadChat) return { id: { in: [] } };
|
|
|
|
const readsAllFiles = canReadFiles && hasPermission(ctx, "files.read_all");
|
|
const readsAllHrFiles = canReadAllHrFiles(ctx);
|
|
const [projectIds, chatMessageIds] = readsAllFiles
|
|
? [[], []]
|
|
: await Promise.all([
|
|
canReadFiles ? loadVisibleLinkedProjectIdsWithTx(tx, tenantId, ctx, candidateFileIds, queryState) : Promise.resolve([]),
|
|
canReadChat ? loadVisibleLinkedChatMessageIdsWithTx(tx, tenantId, ctx, candidateFileIds) : Promise.resolve([]),
|
|
]);
|
|
const scopedBranches: Prisma.FileWhereInput[] = [
|
|
...(canReadFiles
|
|
? [
|
|
{ createdByUserId: ctx.userId },
|
|
{ assignedToUserId: ctx.userId },
|
|
...(projectIds.length > 0 ? [{ links: { some: { tenantId, entityType: "project" as const, entityId: { in: projectIds } } } }] : []),
|
|
{
|
|
links: {
|
|
some: {
|
|
tenantId,
|
|
entityType: "user" as const,
|
|
...(readsAllHrFiles ? {} : { entityId: ctx.userId }),
|
|
},
|
|
},
|
|
},
|
|
]
|
|
: []),
|
|
...(chatMessageIds.length > 0 ? [{ links: { some: { tenantId, entityType: "chat_message" as const, entityId: { in: chatMessageIds } } } }] : []),
|
|
];
|
|
const baseScope: Prisma.FileWhereInput = readsAllFiles ? {} : scopedBranches.length > 0 ? { OR: scopedBranches } : { id: { in: [] } };
|
|
|
|
if (readsAllHrFiles) return baseScope;
|
|
const hrScope: Prisma.FileWhereInput = {
|
|
links: {
|
|
none: {
|
|
tenantId,
|
|
entityType: "user",
|
|
entityId: { not: ctx.userId },
|
|
},
|
|
},
|
|
};
|
|
return Object.keys(baseScope).length === 0 ? hrScope : { AND: [baseScope, hrScope] };
|
|
}
|