BetterFrame/server/src/shared/audit.ts

76 lines
2.2 KiB
TypeScript
Raw Normal View History

/**
* Audit logging helper single inline call from route handlers.
*
* audit(repo, event, "user.login", { result: "ok" });
* audit(repo, event, "firmware.upload", { resource_id: rel.id, metadata: { version, channel } });
*
* Pulls actor + ip out of the h3 event context. Never throws logging
* failure must not break the caller's request.
*/
import type { Repository } from "./db/repository.js";
import type { AuditActorType, AuditResult } from "./types.js";
interface AuditCtx {
context?: {
2026-05-26 05:11:45 +00:00
user?: { id?: string; username?: string };
apiKeyPrefix?: string;
session?: unknown;
};
req?: { headers: { get(name: string): string | null } };
}
export interface AuditInput {
resource_type?: string;
resource_id?: string | number;
metadata?: Record<string, unknown>;
result?: AuditResult;
/** Override actor (e.g. when system performs action on behalf of nobody). */
actor_type?: AuditActorType;
2026-05-26 05:11:45 +00:00
actor_id?: string | null;
actor_label?: string | null;
}
export async function audit(
repo: Repository,
event: AuditCtx | null,
action: string,
input: AuditInput = {},
): Promise<void> {
try {
const ctx = event?.context;
let actor_type: AuditActorType = input.actor_type ?? "system";
2026-05-26 05:11:45 +00:00
let actor_id: string | null = input.actor_id ?? null;
let actor_label: string | null = input.actor_label ?? null;
if (!input.actor_type && ctx) {
if (ctx.apiKeyPrefix) {
actor_type = "api_key";
actor_label = ctx.apiKeyPrefix;
} else if (ctx.user) {
actor_type = "user";
actor_id = ctx.user.id ?? null;
actor_label = ctx.user.username ?? null;
}
}
const headers = event?.req?.headers;
const ip = headers?.get("x-real-ip")
?? headers?.get("x-forwarded-for")?.split(",")[0]?.trim()
?? null;
await repo.insertAudit({
actor_type,
actor_id,
actor_label,
action,
resource_type: input.resource_type ?? null,
resource_id: input.resource_id != null ? String(input.resource_id) : null,
ip,
metadata: input.metadata ?? {},
result: input.result ?? "ok",
});
} catch {
/* never throw from audit */
}
}