- 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>
51 lines
1.7 KiB
TypeScript
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,
|
|
});
|
|
}
|