2026-04-22 18:10:04 +08:00
|
|
|
import { randomUUID } from "node:crypto";
|
2026-03-28 11:19:09 +08:00
|
|
|
import type { FastifyBaseLogger } from "fastify";
|
2026-04-22 18:10:04 +08:00
|
|
|
import { db, schema } from "../db/index.js";
|
2026-03-28 11:19:09 +08:00
|
|
|
|
|
|
|
|
type AuditEvent =
|
|
|
|
|
| "LOGIN_SUCCESS"
|
|
|
|
|
| "LOGIN_FAILED"
|
|
|
|
|
| "LOGOUT"
|
|
|
|
|
| "PASSWORD_CHANGED"
|
|
|
|
|
| "PASSWORD_RESET"
|
|
|
|
|
| "USER_CREATED"
|
|
|
|
|
| "USER_DELETED"
|
|
|
|
|
| "USER_UPDATED"
|
|
|
|
|
| "FILE_UPLOADED"
|
|
|
|
|
| "FILE_DELETED"
|
|
|
|
|
| "API_KEY_CREATED"
|
2026-04-22 18:10:04 +08:00
|
|
|
| "API_KEY_DELETED"
|
|
|
|
|
| "ROLE_CREATED"
|
|
|
|
|
| "ROLE_UPDATED"
|
|
|
|
|
| "ROLE_DELETED"
|
|
|
|
|
| "SETTINGS_UPDATED";
|
2026-03-28 11:19:09 +08:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Emit a structured audit log entry for security-relevant events.
|
|
|
|
|
*
|
2026-04-22 18:10:04 +08:00
|
|
|
* Dual-writes: structured stdout log (for aggregators) + SQLite row.
|
2026-03-28 11:19:09 +08:00
|
|
|
*/
|
|
|
|
|
export function auditLog(
|
|
|
|
|
logger: FastifyBaseLogger,
|
|
|
|
|
event: AuditEvent,
|
|
|
|
|
details: Record<string, unknown> = {},
|
|
|
|
|
): void {
|
|
|
|
|
logger.info({ audit: true, event, ...details }, `[AUDIT] ${event}`);
|
2026-04-22 18:10:04 +08:00
|
|
|
|
|
|
|
|
const actorId = (details.userId as string) ?? (details.adminId as string) ?? null;
|
|
|
|
|
const actorUsername = (details.username as string) ?? (details.newUsername as string) ?? "system";
|
|
|
|
|
const targetId = (details.targetUserId as string) ?? (details.keyId as string) ?? null;
|
|
|
|
|
const targetType = deriveTargetType(event);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
db.insert(schema.auditLog)
|
|
|
|
|
.values({
|
|
|
|
|
id: randomUUID(),
|
|
|
|
|
actorId,
|
|
|
|
|
actorUsername,
|
|
|
|
|
action: event,
|
|
|
|
|
targetType,
|
|
|
|
|
targetId,
|
|
|
|
|
details: JSON.stringify(details),
|
|
|
|
|
ipAddress: null,
|
|
|
|
|
})
|
|
|
|
|
.run();
|
|
|
|
|
} catch {
|
|
|
|
|
logger.warn({ event }, "Failed to write audit log to DB");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function deriveTargetType(event: AuditEvent): string | null {
|
|
|
|
|
if (
|
|
|
|
|
event.startsWith("USER_") ||
|
|
|
|
|
event.startsWith("LOGIN") ||
|
|
|
|
|
event.startsWith("PASSWORD") ||
|
|
|
|
|
event === "LOGOUT"
|
|
|
|
|
)
|
|
|
|
|
return "user";
|
|
|
|
|
if (event.startsWith("API_KEY")) return "api_key";
|
|
|
|
|
if (event.startsWith("FILE")) return "file";
|
|
|
|
|
if (event.startsWith("ROLE")) return "role";
|
|
|
|
|
if (event === "SETTINGS_UPDATED") return "setting";
|
|
|
|
|
return null;
|
2026-03-28 11:19:09 +08:00
|
|
|
}
|