feat : copy buzzy audit logs
CI / Verify (push) Successful in 2m2s
CI / Build immutable images and deploy (push) Successful in 2m47s

This commit is contained in:
2026-09-27 17:00:25 +07:00 Unverified
parent 8ddb5896b6
commit 188cec9bda
10 changed files with 152 additions and 353 deletions
+34 -32
View File
@@ -1,9 +1,9 @@
import "server-only";
import { and, count, desc, eq } from "drizzle-orm";
import { count, desc, eq } from "drizzle-orm";
import { getDb, type Database } from "@/db";
import { auditLogs } from "@/db/schema";
import { auditLog as auditLogTable, type AuditLogValue } from "@/db/schema";
import { securityLog } from "@/lib/security/http";
export const AUDIT_ACTIONS = [
@@ -47,10 +47,7 @@ export const AUDIT_TARGET_TYPES = [
export type AuditAction = (typeof AUDIT_ACTIONS)[number];
export type AuditTargetType = (typeof AUDIT_TARGET_TYPES)[number];
export type AuditMetadata = Record<
string,
string | number | boolean | null
>;
export type AuditMetadata = Record<string, AuditLogValue>;
export interface AuditActor {
id: string;
@@ -84,15 +81,21 @@ export async function writeAuditLog(
actor: AuditActor,
event: AuditLogInput,
): Promise<void> {
await writer.insert(auditLogs).values({
actorId: actor.id,
actorLabel: actor.label,
action: event.action,
targetType: event.targetType,
targetId: event.targetId,
scope: event.scope,
resultingVersion: event.resultingVersion,
metadata: event.metadata ?? {},
await writer.insert(auditLogTable).values({
authorId: actor.id,
author: actor.label,
text: AUDIT_ACTION_LABELS[event.action],
details: {
target: {
type: event.targetType,
id: event.targetId,
},
...(event.scope ? { scope: event.scope } : {}),
...(event.resultingVersion
? { version: event.resultingVersion }
: {}),
...event.metadata,
},
});
}
@@ -112,7 +115,6 @@ export async function writeAuditLogBestEffort(
export interface AuditLogFilters {
actorId?: string;
targetType?: AuditTargetType;
page?: number;
pageSize?: number;
}
@@ -120,25 +122,21 @@ export interface AuditLogFilters {
export async function listAuditLogs(filters: AuditLogFilters = {}) {
const page = Math.max(1, Math.floor(filters.page ?? 1));
const pageSize = Math.max(1, Math.min(100, Math.floor(filters.pageSize ?? 50)));
const conditions = [
filters.actorId ? eq(auditLogs.actorId, filters.actorId) : undefined,
filters.targetType
? eq(auditLogs.targetType, filters.targetType)
: undefined,
].filter((condition) => condition !== undefined);
const where = conditions.length ? and(...conditions) : undefined;
const where = filters.actorId
? eq(auditLogTable.authorId, filters.actorId)
: undefined;
const db = getDb();
const [events, [total]] = await Promise.all([
db
.select()
.from(auditLogs)
.from(auditLogTable)
.where(where)
.orderBy(desc(auditLogs.createdAt), desc(auditLogs.id))
.orderBy(desc(auditLogTable.time), desc(auditLogTable.id))
.limit(pageSize)
.offset((page - 1) * pageSize),
db
.select({ value: count() })
.from(auditLogs)
.from(auditLogTable)
.where(where),
]);
return {
@@ -152,13 +150,17 @@ export async function listAuditLogs(filters: AuditLogFilters = {}) {
export async function listAuditActors() {
const rows = await getDb()
.selectDistinctOn([auditLogs.actorId], {
id: auditLogs.actorId,
label: auditLogs.actorLabel,
.selectDistinctOn([auditLogTable.authorId], {
id: auditLogTable.authorId,
label: auditLogTable.author,
})
.from(auditLogs)
.orderBy(auditLogs.actorId, desc(auditLogs.id));
return rows.sort((left, right) =>
.from(auditLogTable)
.orderBy(auditLogTable.authorId, desc(auditLogTable.id));
const actors = rows.filter(
(row): row is { id: string; label: string } =>
Boolean(row.id && row.label),
);
return actors.sort((left, right) =>
left.label.localeCompare(right.label, "th"),
);
}