85 lines
3.0 KiB
TypeScript
85 lines
3.0 KiB
TypeScript
// P4 Files (AK-P4a) §12 — audit + activity wiring for the Files module.
|
|
//
|
|
// Audit (DoD 13): every write action records a tamper-evident AuditEvent (M001)
|
|
// via foundation.auditEvent with the shared db client.
|
|
//
|
|
// Activity (DoD 14): user-facing timeline entries are logged AGAINST the linked
|
|
// entity (the file itself is not an Activity subject in v1 — see decision 4 in
|
|
// the build plan). Only FileEntityType values that map to an ActivitySubjectType
|
|
// (contact/company/lead/deal/project/task) produce an activity; others are
|
|
// skipped (no shared @hivegrid/activity edit).
|
|
|
|
import type { JsonValue } from "@toolctrl/foundation";
|
|
import { createAuditEvent } from "@toolctrl/db/audit-events.server";
|
|
import { logActivity, type ActivitySubjectType, type ActivityType } from "@hivegrid/activity";
|
|
import type { FilesRouteContext } from "./fileAccess.server";
|
|
import type { FileEntityType } from "./fileService.server";
|
|
|
|
export type FileAuditAction = "file.uploaded" | "file.linked" | "file.unlinked" | "file.archived" | "file.reactivated" | "file.deleted" | "file.updated";
|
|
|
|
export async function auditFileEvent(input: {
|
|
ctx: Pick<FilesRouteContext, "tenantId" | "userId">;
|
|
action: FileAuditAction;
|
|
fileId: string;
|
|
metadata?: Record<string, JsonValue>;
|
|
}): Promise<void> {
|
|
try {
|
|
// createAuditEvent (M001) writes the tamper-evident, hash-chained AuditEvent
|
|
// and defaults its client to the shared db singleton.
|
|
await createAuditEvent({
|
|
tenantId: input.ctx.tenantId,
|
|
actorId: input.ctx.userId,
|
|
actorType: "user",
|
|
action: input.action,
|
|
targetType: "file",
|
|
targetId: input.fileId,
|
|
metadata: input.metadata ?? null,
|
|
});
|
|
} catch {
|
|
// Audit must never break the user action; the failure is logged upstream.
|
|
}
|
|
}
|
|
|
|
const ENTITY_TO_ACTIVITY_SUBJECT: Partial<Record<FileEntityType, ActivitySubjectType>> = {
|
|
contact: "contact",
|
|
company: "company",
|
|
lead: "lead",
|
|
deal: "deal",
|
|
project: "project",
|
|
task: "task",
|
|
};
|
|
|
|
/**
|
|
* Log a file-related activity against the linked entity's timeline. No-op when
|
|
* the entity type has no Activity subject mapping (e.g. user/form_submission).
|
|
*/
|
|
export async function logFileActivity(input: {
|
|
ctx: Pick<FilesRouteContext, "tenantId" | "userId">;
|
|
entityType: FileEntityType;
|
|
entityId: string;
|
|
activityType: Extract<ActivityType, "file_uploaded">;
|
|
summaryText: string;
|
|
fileId: string;
|
|
}): Promise<void> {
|
|
const subjectType = ENTITY_TO_ACTIVITY_SUBJECT[input.entityType];
|
|
if (!subjectType) {
|
|
return;
|
|
}
|
|
try {
|
|
// logActivity defaults its write client to the shared db singleton.
|
|
await logActivity({
|
|
tenantId: input.ctx.tenantId,
|
|
actorUserId: input.ctx.userId,
|
|
actorKind: "user",
|
|
activityType: input.activityType,
|
|
subjectType,
|
|
subjectId: input.entityId,
|
|
summaryText: input.summaryText,
|
|
// The file is not an Activity subject in v1; carry its id in metadata.
|
|
metadata: { fileId: input.fileId },
|
|
});
|
|
} catch {
|
|
// Activity logging is best-effort; never break the user action.
|
|
}
|
|
}
|