Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/apps/web/app/modules/emailInbox/services/emailActivityMasking.server.ts
T
2026-07-08 16:15:37 +02:00

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,
);
}