Files
elegalsoftware/apps/api/src/lib/audit.ts
T
Leon SerfatyandClaude Opus 5 eb36b81dc9 Superadmin impersonation, hardened auth/upload paths, dependency updates
- Add superadmin impersonation: sessions.impersonated_by (migration 0003) is
  stamped onto audit rows so impersonated actions are attributable, with a
  persistent ImpersonationBanner in the app shell.
- Harden auth and upload handling across routes (safe redirect targets,
  filename sanitization, checkout grant handling).
- Update dependencies: Sentry 8 -> 10, @fastify/static 8 -> 10,
  react-router-dom 6.30.6; add find-my-way / fast-uri overrides.
- Add tests for safe-next, checkout-grant, and upload-filename.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 10:25:39 -04:00

51 lines
1.7 KiB
TypeScript

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