BetterFrame/server/src/shared/audit.ts
Mitchell R 64f47a9a6b
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.

Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
  RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
  u32 (old) and String (new) IDs for backward compatibility

Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 07:11:45 +02:00

75 lines
2.2 KiB
TypeScript

/**
* 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?: {
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;
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";
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 */
}
}