71 lines
2.6 KiB
TypeScript
71 lines
2.6 KiB
TypeScript
// VA-P23 · Activity-Stream permission masking (M09 Block H, DoD 6).
|
|
//
|
|
// email_received/email_sent activities on a Kontakt/Lead/Projekt timeline are
|
|
// visible only to users who may see the underlying mailbox: `email.read_all`
|
|
// sees everything; `email.read_own` sees activities of their OWN mailboxes
|
|
// (metadata.mailboxId); everyone else gets the Klartext placeholder
|
|
// "E-Mail-Aktivität (nicht einsehbar)" WITHOUT subject/snippet details.
|
|
|
|
import { db } from "~/utils/db.server";
|
|
import { EMAIL_PERMISSIONS } from "../routes/emailInboxAccess.server";
|
|
|
|
const EMAIL_ACTIVITY_PREFIX = "email_";
|
|
|
|
export interface MaskableActivityRow {
|
|
activityType: string;
|
|
summaryText: string;
|
|
metadata?: unknown;
|
|
}
|
|
|
|
/** Pure decision: mask this row for a viewer with the given visibility? */
|
|
export function shouldMaskEmailActivity(
|
|
row: MaskableActivityRow,
|
|
viewer: { canReadAll: boolean; canReadOwn: boolean; ownMailboxIds: ReadonlySet<string> },
|
|
): boolean {
|
|
if (!row.activityType.startsWith(EMAIL_ACTIVITY_PREFIX)) {
|
|
return false;
|
|
}
|
|
if (viewer.canReadAll) {
|
|
return false;
|
|
}
|
|
if (!viewer.canReadOwn) {
|
|
return true;
|
|
}
|
|
const mailboxId = (row.metadata as { mailboxId?: unknown } | null | undefined)?.mailboxId;
|
|
// Conservative: an email activity without a mailbox reference is masked too.
|
|
return typeof mailboxId !== "string" || !viewer.ownMailboxIds.has(mailboxId);
|
|
}
|
|
|
|
/**
|
|
* Mask email activities in-place-safe (returns new row objects where masked).
|
|
* `maskedText` is the pre-translated Klartext placeholder from the caller.
|
|
*/
|
|
export async function maskEmailActivitiesForViewer<T extends MaskableActivityRow>(input: {
|
|
tenantId: string;
|
|
userId: string;
|
|
permissions: readonly string[];
|
|
rows: readonly T[];
|
|
maskedText: string;
|
|
}): Promise<T[]> {
|
|
const hasEmailRows = input.rows.some((row) => row.activityType.startsWith(EMAIL_ACTIVITY_PREFIX));
|
|
const canReadAll = input.permissions.includes(EMAIL_PERMISSIONS.readAll);
|
|
if (!hasEmailRows || canReadAll) {
|
|
return [...input.rows];
|
|
}
|
|
const canReadOwn = input.permissions.includes(EMAIL_PERMISSIONS.readOwn);
|
|
const ownMailboxIds = canReadOwn
|
|
? new Set(
|
|
(
|
|
await db.emailIntegrationMailbox.findMany({
|
|
where: { tenantId: input.tenantId, ownerUserId: input.userId, archivedAt: null },
|
|
select: { id: true },
|
|
})
|
|
).map((m) => m.id),
|
|
)
|
|
: new Set<string>();
|
|
const viewer = { canReadAll, canReadOwn, ownMailboxIds };
|
|
return input.rows.map((row) =>
|
|
shouldMaskEmailActivity(row, viewer) ? { ...row, summaryText: input.maskedText, metadata: undefined } : row,
|
|
);
|
|
}
|