import { all, run } from "./db"; import { randomId } from "./crypto"; export type AdminAuditEntry = { id: string; actor_id: string | null; actor_name: string | null; action: string; target_kind: string; target_id: string; reason: string | null; created_at: number; }; export type AdminAuditInput = { actorId: string; action: string; targetKind: string; targetId: string; reason?: string | null; }; const MAX_REASON = 1000; /** Appends one site-admin action to the audit log. */ export async function recordAdminAction(input: AdminAuditInput): Promise { await run( `INSERT INTO admin_audit_log (id, actor_id, action, target_kind, target_id, reason, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)`, randomId(), input.actorId, input.action, input.targetKind, input.targetId, input.reason?.trim().slice(0, MAX_REASON) || null, Date.now(), ); } /** Keyset position: entries strictly after this one in (created_at DESC, id DESC) order. */ export type AdminAuditCursor = { createdAt: number; id: string }; /** One action can write several rows in the same millisecond, so pages are keyed on (created_at, id), not time. */ export async function listAdminAudit( filter: { targetKind?: string; targetId?: string; after?: AdminAuditCursor } = {}, limit = 100, ): Promise { return all( `SELECT l.id, l.actor_id, a.name AS actor_name, l.action, l.target_kind, l.target_id, l.reason, l.created_at FROM admin_audit_log l LEFT JOIN accounts a ON a.id = l.actor_id WHERE (?1 IS NULL OR l.target_kind = ?1) AND (?2 IS NULL OR l.target_id = ?2) AND (?3 IS NULL OR l.created_at < ?3 OR (l.created_at = ?3 AND l.id < ?4)) ORDER BY l.created_at DESC, l.id DESC LIMIT ?5`, filter.targetKind ?? null, filter.targetId ?? null, filter.after?.createdAt ?? null, filter.after?.id ?? "", limit, ); }