import type { FastifyRequest } from 'fastify'; import { getDb, auditLog } from '@lawdesk/db'; export interface AuditEntry { userId?: string | null; firmId?: string | null; action: string; meta?: unknown; ip?: string | null; /** Superadmin id when the acting session is an impersonation. */ impersonatedBy?: string | null; } export async function logAudit(entry: AuditEntry): Promise { // The impersonating admin is folded into `meta` rather than a dedicated column so existing // rows stay valid: an entry written during impersonation records BOTH the user the action // appears to come from and the admin who actually performed it. const meta = entry.impersonatedBy != null ? { ...(typeof entry.meta === 'object' && entry.meta !== null ? entry.meta : { value: entry.meta }), impersonatedBy: entry.impersonatedBy } : entry.meta; await getDb().insert(auditLog).values({ userId: entry.userId ?? null, firmId: entry.firmId ?? null, action: entry.action, meta: meta == null ? null : JSON.stringify(meta), ip: entry.ip ?? null, }); } /** * Audit an action performed by the current request's user. Carries the impersonating admin * through automatically, so a support session can never write an audit trail that looks like * the customer acted alone. */ export async function logAuditFromRequest( req: FastifyRequest, action: string, extra: { firmId?: string | null; meta?: unknown } = {}, ): Promise { await logAudit({ userId: req.user?.id ?? null, firmId: extra.firmId !== undefined ? extra.firmId : (req.user?.firmId ?? null), action, meta: extra.meta, ip: req.ip, impersonatedBy: req.user?.impersonatedBy ?? null, }); }