feat(audit): локальный журнал и push в auth-portal
Build and Push CFDM Docker Image / build-and-push (push) Successful in 2m9s
Build and Push CFDM Docker Image / create-release (push) Skipped
Build and Push CFDM Docker Image / update-wiki (push) Successful in 5s

Таблица audit_log, recordAudit на CRUD, GET /api/v1/audit и dual-write source_app=cfdm.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
DenozordecandCursor committed 2026-07-21 13:24:32 +07:00
1 parent 30447d8380
commit 859ad23006
22 files changed
+1561 -106

No files matched your search

+131
View File
@@ -0,0 +1,131 @@
import { and, desc, eq, or } from "drizzle-orm";
import { randomUUID } from "node:crypto";
import type {
AuditLogEntry,
AuditSeverity,
AuditSourceApp,
AuditTargetType,
} from "@cfdm/shared";
import type { Db } from "./client.js";
import { auditLog } from "./schema.js";
export type AppendAuditInput = {
eventId?: string | null;
sourceApp?: AuditSourceApp;
action: string;
severity?: AuditSeverity;
actorUserId?: string | null;
actorEmail?: string | null;
actorName?: string | null;
targetType?: AuditTargetType | null;
targetId?: string | null;
summary: string;
details?: Record<string, unknown> | null;
ip?: string | null;
createdAt?: string | null;
};
function mapRow(row: typeof auditLog.$inferSelect): AuditLogEntry {
let details: Record<string, unknown> | null = null;
if (row.details_json) {
try {
details = JSON.parse(row.details_json) as Record<string, unknown>;
} catch {
details = { raw: row.details_json };
}
}
return {
id: row.id,
event_id: row.event_id,
source_app: (row.source_app as AuditSourceApp) || "cfdm",
action: row.action,
severity: row.severity as AuditSeverity,
actor_user_id: row.actor_user_id,
actor_email: row.actor_email,
actor_name: row.actor_name,
target_type: (row.target_type as AuditTargetType | null) ?? null,
target_id: row.target_id,
summary: row.summary,
details,
ip: row.ip,
created_at: row.created_at,
};
}
/** @returns true if inserted, false if duplicate event_id */
export function appendAudit(db: Db, input: AppendAuditInput): boolean {
const now = input.createdAt ?? new Date().toISOString();
const eventId = input.eventId ?? null;
if (eventId) {
const existing = db
.select({ id: auditLog.id })
.from(auditLog)
.where(eq(auditLog.event_id, eventId))
.get();
if (existing) return false;
}
db.insert(auditLog)
.values({
id: randomUUID(),
event_id: eventId,
source_app: input.sourceApp ?? "cfdm",
action: input.action,
severity: input.severity ?? "info",
actor_user_id: input.actorUserId ?? null,
actor_email: input.actorEmail ?? null,
actor_name: input.actorName ?? null,
target_type: input.targetType ?? null,
target_id: input.targetId ?? null,
summary: input.summary,
details_json: input.details ? JSON.stringify(input.details) : null,
ip: input.ip ?? null,
created_at: now,
})
.run();
return true;
}
export function listAudit(
db: Db,
opts: {
action?: string;
severity?: AuditSeverity;
userId?: string;
sourceApp?: AuditSourceApp;
limit?: number;
} = {},
): AuditLogEntry[] {
const limit = opts.limit ?? 200;
const conditions = [];
if (opts.action) conditions.push(eq(auditLog.action, opts.action));
if (opts.severity) conditions.push(eq(auditLog.severity, opts.severity));
if (opts.sourceApp) conditions.push(eq(auditLog.source_app, opts.sourceApp));
if (opts.userId) {
conditions.push(
or(
eq(auditLog.actor_user_id, opts.userId),
eq(auditLog.target_id, opts.userId),
)!,
);
}
const rows =
conditions.length > 0
? db
.select()
.from(auditLog)
.where(and(...conditions))
.orderBy(desc(auditLog.created_at))
.limit(limit)
.all()
: db
.select()
.from(auditLog)
.orderBy(desc(auditLog.created_at))
.limit(limit)
.all();
return rows.map(mapRow);
}
+1
View File
@@ -1,6 +1,7 @@
export * from "./schema.js";
export * from "./client.js";
export * from "./errors.js";
export * from "./audit-log.js";
export * from "./settings-repo.js";
export * as repos from "./repos.js";
export type { DnsListFilter, UpdateSubdomainPatch } from "./repos.js";
+20
View File
@@ -359,6 +359,25 @@ export const notificationLog = sqliteTable("notification_log", {
.default(sql`datetime('now')`),
});
export const auditLog = sqliteTable("audit_log", {
id: text("id").primaryKey(),
event_id: text("event_id"),
source_app: text("source_app").notNull().default("cfdm"),
action: text("action").notNull(),
severity: text("severity").notNull().default("info"),
actor_user_id: text("actor_user_id"),
actor_email: text("actor_email"),
actor_name: text("actor_name"),
target_type: text("target_type"),
target_id: text("target_id"),
summary: text("summary").notNull(),
details_json: text("details_json"),
ip: text("ip"),
created_at: text("created_at")
.notNull()
.default(sql`datetime('now')`),
});
export const schema = {
groups,
services,
@@ -379,4 +398,5 @@ export const schema = {
domainMonitors,
domainMonitorResults,
notificationLog,
auditLog,
};