Таблица audit_log, recordAudit на CRUD, GET /api/v1/audit и dual-write source_app=cfdm. Co-authored-by: Cursor <cursoragent@cursor.com>
102 lines
2.5 KiB
TypeScript
102 lines
2.5 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
import type { FastifyInstance, FastifyRequest } from "fastify";
|
|
import { appendAudit, type AppendAuditInput } from "@cfdm/db";
|
|
import { pushAuditEvents } from "../services/audit-portal-push.js";
|
|
|
|
export function clientIp(request: FastifyRequest): string | null {
|
|
const forwarded = request.headers["x-forwarded-for"];
|
|
if (typeof forwarded === "string" && forwarded.trim()) {
|
|
return forwarded.split(",")[0]?.trim() ?? null;
|
|
}
|
|
return request.ip ?? null;
|
|
}
|
|
|
|
export function actorFromRequest(
|
|
request: FastifyRequest,
|
|
): Pick<AppendAuditInput, "actorUserId" | "actorEmail" | "actorName"> {
|
|
const u = request.authUser;
|
|
if (u) {
|
|
return {
|
|
actorUserId: u.id,
|
|
actorEmail: u.email || null,
|
|
actorName: u.name || null,
|
|
};
|
|
}
|
|
|
|
const payload = request.user;
|
|
if (payload?.sub) {
|
|
return {
|
|
actorUserId: String(payload.sub),
|
|
actorEmail: payload.email ? String(payload.email) : null,
|
|
actorName: payload.name ? String(payload.name) : null,
|
|
};
|
|
}
|
|
|
|
return {
|
|
actorUserId: null,
|
|
actorEmail: null,
|
|
actorName: null,
|
|
};
|
|
}
|
|
|
|
type RecordAuditInput = Omit<
|
|
AppendAuditInput,
|
|
| "sourceApp"
|
|
| "eventId"
|
|
| "ip"
|
|
| "actorUserId"
|
|
| "actorEmail"
|
|
| "actorName"
|
|
>;
|
|
|
|
/** Local append + optional async push to auth-portal ingest. */
|
|
export function recordAudit(
|
|
app: FastifyInstance,
|
|
request: FastifyRequest,
|
|
input: RecordAuditInput,
|
|
): void {
|
|
const eventId = randomUUID();
|
|
const actor = actorFromRequest(request);
|
|
const ip = clientIp(request);
|
|
const createdAt = input.createdAt ?? new Date().toISOString();
|
|
|
|
const full: AppendAuditInput = {
|
|
...input,
|
|
...actor,
|
|
eventId,
|
|
sourceApp: "cfdm",
|
|
ip,
|
|
createdAt,
|
|
};
|
|
|
|
try {
|
|
appendAudit(app.db, full);
|
|
} catch (err) {
|
|
app.log.warn({ err }, "audit_log append failed");
|
|
}
|
|
|
|
const secret = app.config.authAuditIngestSecret;
|
|
const portalUrl = app.config.authPortalUrl;
|
|
if (!secret || !portalUrl) return;
|
|
|
|
void pushAuditEvents(portalUrl, secret, [
|
|
{
|
|
event_id: eventId,
|
|
source_app: "cfdm",
|
|
action: input.action,
|
|
severity: input.severity,
|
|
actor_user_id: actor.actorUserId,
|
|
actor_email: actor.actorEmail,
|
|
actor_name: actor.actorName,
|
|
target_type: input.targetType,
|
|
target_id: input.targetId,
|
|
summary: input.summary,
|
|
details: input.details,
|
|
ip,
|
|
created_at: createdAt,
|
|
},
|
|
]).catch((err) => {
|
|
app.log.warn({ err, eventId }, "audit portal push failed");
|
|
});
|
|
}
|