feat: add request correlation IDs to audit logs and response headers

This commit is contained in:
SnapOtter
2026-06-13 17:04:27 +08:00
parent c1dc27f248
commit 1cf1f47d6f
10 changed files with 81 additions and 68 deletions
+2
View File
@@ -182,6 +182,7 @@ function parseTrustProxy(value: string): boolean | number | string {
} }
const app = Fastify({ const app = Fastify({
genReqId: (req) => (req.headers["x-request-id"] as string) ?? randomUUID(),
logger: { logger: {
level: env.LOG_LEVEL, level: env.LOG_LEVEL,
transport: { transport: {
@@ -255,6 +256,7 @@ await app.register(cors, {
// HTTP so it is safe (and desirable) to send it in dev/staging too. CSP catches // HTTP so it is safe (and desirable) to send it in dev/staging too. CSP catches
// injection issues early when applied during development. // injection issues early when applied during development.
app.addHook("onSend", async (_request, reply) => { app.addHook("onSend", async (_request, reply) => {
reply.header("x-request-id", _request.id);
reply.header("X-Content-Type-Options", "nosniff"); reply.header("X-Content-Type-Options", "nosniff");
reply.header("X-Frame-Options", "DENY"); reply.header("X-Frame-Options", "DENY");
reply.header("X-XSS-Protection", "0"); reply.header("X-XSS-Protection", "0");
+14 -2
View File
@@ -1,6 +1,6 @@
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import type { FastifyBaseLogger } from "fastify"; import type { FastifyBaseLogger, FastifyRequest } from "fastify";
import { env } from "../config.js"; import { env } from "../config.js";
import { db, schema } from "../db/index.js"; import { db, schema } from "../db/index.js";
import { computeHmac } from "./audit-integrity.js"; import { computeHmac } from "./audit-integrity.js";
@@ -51,8 +51,9 @@ export async function auditLog(
event: string, event: string,
details: Record<string, unknown> = {}, details: Record<string, unknown> = {},
ip: string | null = null, ip: string | null = null,
requestId: string | null = null,
): Promise<void> { ): Promise<void> {
logger.info({ audit: true, event, ip, ...details }, `[AUDIT] ${event}`); logger.info({ audit: true, event, ip, requestId, ...details }, `[AUDIT] ${event}`);
const actorId = (details.userId as string) ?? (details.adminId as string) ?? null; const actorId = (details.userId as string) ?? (details.adminId as string) ?? null;
const actorUsername = (details.username as string) ?? (details.newUsername as string) ?? "system"; const actorUsername = (details.username as string) ?? (details.newUsername as string) ?? "system";
@@ -70,6 +71,7 @@ export async function auditLog(
targetId, targetId,
details, details,
ipAddress: ip, ipAddress: ip,
requestId,
}); });
} catch { } catch {
logger.warn({ event }, "Failed to write audit log to DB"); logger.warn({ event }, "Failed to write audit log to DB");
@@ -95,6 +97,7 @@ export async function auditLog(
targetId, targetId,
details, details,
ipAddress: ip, ipAddress: ip,
requestId,
}; };
const integrity = computeHmac(rowData, hmacKey); const integrity = computeHmac(rowData, hmacKey);
await db await db
@@ -108,6 +111,15 @@ export async function auditLog(
} }
} }
/**
* Create a bound audit logger from a Fastify request.
* Captures request.ip and request.id so call sites only need event + details.
*/
export function auditFromRequest(request: FastifyRequest) {
return (event: string, details: Record<string, unknown> = {}) =>
auditLog(request.log, event, details, request.ip, request.id);
}
function deriveTargetType(event: string): string | null { function deriveTargetType(event: string): string | null {
if ( if (
event.startsWith("USER_") || event.startsWith("USER_") ||
+19 -17
View File
@@ -5,7 +5,7 @@ import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import { z } from "zod"; import { z } from "zod";
import { env } from "../config.js"; import { env } from "../config.js";
import { db, schema } from "../db/index.js"; import { db, schema } from "../db/index.js";
import { auditLog, sanitizeAuditInput } from "../lib/audit.js"; import { auditFromRequest, sanitizeAuditInput } from "../lib/audit.js";
import { getPermissions, requirePermission } from "../permissions.js"; import { getPermissions, requirePermission } from "../permissions.js";
const scryptAsync = promisify(scrypt); const scryptAsync = promisify(scrypt);
@@ -290,20 +290,22 @@ export async function authRoutes(app: FastifyInstance): Promise<void> {
.from(schema.users) .from(schema.users)
.where(eq(schema.users.username, body.username)); .where(eq(schema.users.username, body.username));
const audit = auditFromRequest(request);
if (!user || !user.passwordHash) { if (!user || !user.passwordHash) {
await auditLog(request.log, "LOGIN_FAILED", { await audit("LOGIN_FAILED", {
username: sanitizeAuditInput(body.username), username: sanitizeAuditInput(body.username),
reason: "unknown_user", reason: "unknown_user",
}, request.ip); });
return reply.status(401).send({ error: "Invalid credentials" }); return reply.status(401).send({ error: "Invalid credentials" });
} }
const valid = await verifyPassword(body.password, user.passwordHash); const valid = await verifyPassword(body.password, user.passwordHash);
if (!valid) { if (!valid) {
await auditLog(request.log, "LOGIN_FAILED", { await audit("LOGIN_FAILED", {
username: sanitizeAuditInput(body.username), username: sanitizeAuditInput(body.username),
reason: "bad_password", reason: "bad_password",
}, request.ip); });
return reply.status(401).send({ error: "Invalid credentials" }); return reply.status(401).send({ error: "Invalid credentials" });
} }
@@ -317,7 +319,7 @@ export async function authRoutes(app: FastifyInstance): Promise<void> {
expiresAt, expiresAt,
}); });
await auditLog(request.log, "LOGIN_SUCCESS", { userId: user.id, username: user.username }, request.ip); await audit("LOGIN_SUCCESS", { userId: user.id, username: user.username });
const [teamRow] = await db.select().from(schema.teams).where(eq(schema.teams.id, user.team)); const [teamRow] = await db.select().from(schema.teams).where(eq(schema.teams.id, user.team));
@@ -378,7 +380,7 @@ export async function authRoutes(app: FastifyInstance): Promise<void> {
cookieReply.clearCookie("snapotter-session", { path: "/" }); cookieReply.clearCookie("snapotter-session", { path: "/" });
} }
await auditLog(request.log, "LOGOUT", { userId: user?.id }, request.ip); await auditFromRequest(request)("LOGOUT", { userId: user?.id });
return reply.send({ ok: true, ...(logoutUrl && { logoutUrl }) }); return reply.send({ ok: true, ...(logoutUrl && { logoutUrl }) });
}); });
@@ -500,10 +502,10 @@ export async function authRoutes(app: FastifyInstance): Promise<void> {
// Revoke all API keys - if credentials were compromised, keys must be rotated too // Revoke all API keys - if credentials were compromised, keys must be rotated too
await db.delete(schema.apiKeys).where(eq(schema.apiKeys.userId, authUser.id)); await db.delete(schema.apiKeys).where(eq(schema.apiKeys.userId, authUser.id));
await auditLog(request.log, "PASSWORD_CHANGED", { await auditFromRequest(request)("PASSWORD_CHANGED", {
userId: authUser.id, userId: authUser.id,
username: authUser.username, username: authUser.username,
}, request.ip); });
return reply.send({ ok: true }); return reply.send({ ok: true });
}); });
@@ -669,12 +671,12 @@ export async function authRoutes(app: FastifyInstance): Promise<void> {
mustChangePassword: true, mustChangePassword: true,
}); });
await auditLog(request.log, "USER_CREATED", { await auditFromRequest(request)("USER_CREATED", {
adminId: admin.id, adminId: admin.id,
newUserId: id, newUserId: id,
newUsername: body.username, newUsername: body.username,
role, role,
}, request.ip); });
return reply.status(201).send({ return reply.status(201).send({
id, id,
@@ -784,11 +786,11 @@ export async function authRoutes(app: FastifyInstance): Promise<void> {
); );
} }
await auditLog(request.log, "USER_UPDATED", { await auditFromRequest(request)("USER_UPDATED", {
adminId: admin.id, adminId: admin.id,
targetUserId: id, targetUserId: id,
changes: { role: updates.role, team: updates.team }, changes: { role: updates.role, team: updates.team },
}, request.ip); });
return reply.send({ ok: true }); return reply.send({ ok: true });
}, },
@@ -845,11 +847,11 @@ export async function authRoutes(app: FastifyInstance): Promise<void> {
// Revoke all API keys // Revoke all API keys
await db.delete(schema.apiKeys).where(eq(schema.apiKeys.userId, id)); await db.delete(schema.apiKeys).where(eq(schema.apiKeys.userId, id));
await auditLog(request.log, "PASSWORD_RESET", { await auditFromRequest(request)("PASSWORD_RESET", {
adminId: admin.id, adminId: admin.id,
targetUserId: id, targetUserId: id,
targetUsername: user.username, targetUsername: user.username,
}, request.ip); });
return reply.send({ ok: true }); return reply.send({ ok: true });
}, },
@@ -883,11 +885,11 @@ export async function authRoutes(app: FastifyInstance): Promise<void> {
// Delete the user (cascades to api_keys via FK) // Delete the user (cascades to api_keys via FK)
await db.delete(schema.users).where(eq(schema.users.id, id)); await db.delete(schema.users).where(eq(schema.users.id, id));
await auditLog(request.log, "USER_DELETED", { await auditFromRequest(request)("USER_DELETED", {
adminId: admin.id, adminId: admin.id,
deletedUserId: id, deletedUserId: id,
deletedUsername: user.username, deletedUsername: user.username,
}, request.ip); });
return reply.send({ ok: true }); return reply.send({ ok: true });
}, },
+15 -13
View File
@@ -5,7 +5,7 @@ import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import * as oidc from "openid-client"; import * as oidc from "openid-client";
import { env } from "../config.js"; import { env } from "../config.js";
import { db, schema } from "../db/index.js"; import { db, schema } from "../db/index.js";
import { auditLog, sanitizeAuditInput } from "../lib/audit.js"; import { auditFromRequest, sanitizeAuditInput } from "../lib/audit.js";
import { createSessionToken } from "./auth.js"; import { createSessionToken } from "./auth.js";
// ── Types ───────────────────────────────────────────────────────── // ── Types ─────────────────────────────────────────────────────────
@@ -221,15 +221,17 @@ export async function oidcRoutes(app: FastifyInstance): Promise<void> {
return redirectToLogin(reply, "oidc_session_expired"); return redirectToLogin(reply, "oidc_session_expired");
} }
const audit = auditFromRequest(request);
// Check for error response from the IdP // Check for error response from the IdP
if (query.error) { if (query.error) {
request.log.warn( request.log.warn(
{ error: query.error, description: query.error_description }, { error: query.error, description: query.error_description },
"OIDC IdP returned error", "OIDC IdP returned error",
); );
await auditLog(request.log, "OIDC_LOGIN_FAILED", { await audit("OIDC_LOGIN_FAILED", {
reason: sanitizeAuditInput(String(query.error)), reason: sanitizeAuditInput(String(query.error)),
}, request.ip); });
return redirectToLogin(reply, "oidc_auth_failed"); return redirectToLogin(reply, "oidc_auth_failed");
} }
@@ -257,7 +259,7 @@ export async function oidcRoutes(app: FastifyInstance): Promise<void> {
}); });
} catch (err) { } catch (err) {
request.log.error({ err }, "OIDC token exchange failed"); request.log.error({ err }, "OIDC token exchange failed");
await auditLog(request.log, "OIDC_LOGIN_FAILED", { reason: "token_exchange_failed" }, request.ip); await audit("OIDC_LOGIN_FAILED", { reason: "token_exchange_failed" });
return redirectToLogin(reply, "oidc_auth_failed"); return redirectToLogin(reply, "oidc_auth_failed");
} }
@@ -265,7 +267,7 @@ export async function oidcRoutes(app: FastifyInstance): Promise<void> {
const claims = tokenResponse.claims(); const claims = tokenResponse.claims();
if (!claims) { if (!claims) {
request.log.error("OIDC callback: no ID token claims"); request.log.error("OIDC callback: no ID token claims");
await auditLog(request.log, "OIDC_LOGIN_FAILED", { reason: "no_id_token" }, request.ip); await audit("OIDC_LOGIN_FAILED", { reason: "no_id_token" });
return redirectToLogin(reply, "oidc_auth_failed"); return redirectToLogin(reply, "oidc_auth_failed");
} }
@@ -314,11 +316,11 @@ export async function oidcRoutes(app: FastifyInstance): Promise<void> {
}) })
.where(eq(schema.users.id, existingByEmail.id)); .where(eq(schema.users.id, existingByEmail.id));
userId = existingByEmail.id; userId = existingByEmail.id;
await auditLog(request.log, "OIDC_USER_LINKED", { await audit("OIDC_USER_LINKED", {
userId: existingByEmail.id, userId: existingByEmail.id,
username: existingByEmail.username, username: existingByEmail.username,
email, email,
}, request.ip); });
} }
} }
@@ -356,21 +358,21 @@ export async function oidcRoutes(app: FastifyInstance): Promise<void> {
}); });
userId = newUserId; userId = newUserId;
await auditLog(request.log, "OIDC_USER_CREATED", { await audit("OIDC_USER_CREATED", {
userId: newUserId, userId: newUserId,
username: uniqueUsername, username: uniqueUsername,
email, email,
role: env.OIDC_DEFAULT_ROLE, role: env.OIDC_DEFAULT_ROLE,
}, request.ip); });
} }
// 4d. No user found and no auto-create // 4d. No user found and no auto-create
if (!userId) { if (!userId) {
request.log.warn({ sub, email }, "OIDC user not authorized"); request.log.warn({ sub, email }, "OIDC user not authorized");
await auditLog(request.log, "OIDC_LOGIN_FAILED", { await audit("OIDC_LOGIN_FAILED", {
reason: "user_not_authorized", reason: "user_not_authorized",
sub: sanitizeAuditInput(String(sub)), sub: sanitizeAuditInput(String(sub)),
}, request.ip); });
return redirectToLogin(reply, "oidc_user_not_authorized"); return redirectToLogin(reply, "oidc_user_not_authorized");
} }
@@ -388,10 +390,10 @@ export async function oidcRoutes(app: FastifyInstance): Promise<void> {
// Fetch the user for audit logging // Fetch the user for audit logging
const [user] = await db.select().from(schema.users).where(eq(schema.users.id, userId)); const [user] = await db.select().from(schema.users).where(eq(schema.users.id, userId));
await auditLog(request.log, "OIDC_LOGIN_SUCCESS", { await audit("OIDC_LOGIN_SUCCESS", {
userId, userId,
username: user?.username ?? username, username: user?.username ?? username,
}, request.ip); });
// 6. Set session cookie // 6. Set session cookie
reply.setCookie("snapotter-session", token, { reply.setCookie("snapotter-session", token, {
+3 -3
View File
@@ -10,7 +10,7 @@ import { and, eq } from "drizzle-orm";
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import { z } from "zod"; import { z } from "zod";
import { db, schema } from "../db/index.js"; import { db, schema } from "../db/index.js";
import { auditLog } from "../lib/audit.js"; import { auditFromRequest } from "../lib/audit.js";
import { getPermissions, hasEffectivePermission } from "../permissions.js"; import { getPermissions, hasEffectivePermission } from "../permissions.js";
import { computeKeyPrefix, hashPassword, requireAuth } from "../plugins/auth.js"; import { computeKeyPrefix, hashPassword, requireAuth } from "../plugins/auth.js";
@@ -86,7 +86,7 @@ export async function apiKeyRoutes(app: FastifyInstance): Promise<void> {
return reply.status(409).send({ error: "Failed to create API key" }); return reply.status(409).send({ error: "Failed to create API key" });
} }
await auditLog(request.log, "API_KEY_CREATED", { userId: user.id, keyId: id, keyName: name }, request.ip); await auditFromRequest(request)("API_KEY_CREATED", { userId: user.id, keyId: id, keyName: name });
// Return the raw key ONCE — it cannot be retrieved again // Return the raw key ONCE — it cannot be retrieved again
return reply.status(201).send({ return reply.status(201).send({
@@ -155,7 +155,7 @@ export async function apiKeyRoutes(app: FastifyInstance): Promise<void> {
await db.delete(schema.apiKeys).where(eq(schema.apiKeys.id, id)); await db.delete(schema.apiKeys).where(eq(schema.apiKeys.id, id));
await auditLog(request.log, "API_KEY_DELETED", { userId: user.id, keyId: id }, request.ip); await auditFromRequest(request)("API_KEY_DELETED", { userId: user.id, keyId: id });
return reply.send({ ok: true }); return reply.send({ ok: true });
}, },
+3 -3
View File
@@ -3,7 +3,7 @@ import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import { z } from "zod"; import { z } from "zod";
import { env } from "../../config.js"; import { env } from "../../config.js";
import { db, schema } from "../../db/index.js"; import { db, schema } from "../../db/index.js";
import { auditLog } from "../../lib/audit.js"; import { auditFromRequest } from "../../lib/audit.js";
import { encrypt, isEncrypted } from "../../lib/encryption.js"; import { encrypt, isEncrypted } from "../../lib/encryption.js";
import { requirePermission } from "../../permissions.js"; import { requirePermission } from "../../permissions.js";
@@ -115,11 +115,11 @@ export async function registerSiemRoutes(app: FastifyInstance): Promise<void> {
await db.insert(schema.settings).values({ key: SETTINGS_KEY, value }); await db.insert(schema.settings).values({ key: SETTINGS_KEY, value });
} }
await auditLog(request.log, "SETTINGS_UPDATED", { await auditFromRequest(request)("SETTINGS_UPDATED", {
adminId: user.id, adminId: user.id,
username: user.username, username: user.username,
keys: [SETTINGS_KEY], keys: [SETTINGS_KEY],
}, request.ip); });
return reply.send({ ok: true }); return reply.send({ ok: true });
}, },
+5 -5
View File
@@ -4,7 +4,7 @@ import { eq, sql } from "drizzle-orm";
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import { z } from "zod"; import { z } from "zod";
import { db, schema } from "../db/index.js"; import { db, schema } from "../db/index.js";
import { auditLog } from "../lib/audit.js"; import { auditFromRequest } from "../lib/audit.js";
import { requirePermission } from "../permissions.js"; import { requirePermission } from "../permissions.js";
const ALL_PERMISSIONS: Permission[] = [ const ALL_PERMISSIONS: Permission[] = [
@@ -116,7 +116,7 @@ export async function rolesRoutes(app: FastifyInstance): Promise<void> {
createdBy: user.id, createdBy: user.id,
}); });
await auditLog(request.log, "ROLE_CREATED", { adminId: user.id, roleId: id, roleName: name }, request.ip); await auditFromRequest(request)("ROLE_CREATED", { adminId: user.id, roleId: id, roleName: name });
return reply.status(201).send({ return reply.status(201).send({
id, id,
@@ -185,7 +185,7 @@ export async function rolesRoutes(app: FastifyInstance): Promise<void> {
} }
await tx.update(schema.roles).set(updates).where(eq(schema.roles.id, id)); await tx.update(schema.roles).set(updates).where(eq(schema.roles.id, id));
}); });
await auditLog(request.log, "ROLE_UPDATED", { adminId: user.id, roleId: id }, request.ip); await auditFromRequest(request)("ROLE_UPDATED", { adminId: user.id, roleId: id });
return reply.send({ ok: true }); return reply.send({ ok: true });
}, },
@@ -216,11 +216,11 @@ export async function rolesRoutes(app: FastifyInstance): Promise<void> {
.where(eq(schema.users.role, role.name)); .where(eq(schema.users.role, role.name));
await tx.delete(schema.roles).where(eq(schema.roles.id, id)); await tx.delete(schema.roles).where(eq(schema.roles.id, id));
}); });
await auditLog(request.log, "ROLE_DELETED", { await auditFromRequest(request)("ROLE_DELETED", {
adminId: user.id, adminId: user.id,
roleId: id, roleId: id,
roleName: role.name, roleName: role.name,
}, request.ip); });
return reply.send({ ok: true }); return reply.send({ ok: true });
}, },
+3 -3
View File
@@ -10,7 +10,7 @@ import { eq } from "drizzle-orm";
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import { z } from "zod"; import { z } from "zod";
import { db, schema } from "../db/index.js"; import { db, schema } from "../db/index.js";
import { auditLog } from "../lib/audit.js"; import { auditFromRequest } from "../lib/audit.js";
import { env } from "../config.js"; import { env } from "../config.js";
import { encrypt, decrypt, isEncrypted } from "../lib/encryption.js"; import { encrypt, decrypt, isEncrypted } from "../lib/encryption.js";
import { requirePermission } from "../permissions.js"; import { requirePermission } from "../permissions.js";
@@ -115,11 +115,11 @@ export async function settingsRoutes(app: FastifyInstance): Promise<void> {
} }
if (entries.length > 0) { if (entries.length > 0) {
await auditLog(request.log, "SETTINGS_UPDATED", { await auditFromRequest(request)("SETTINGS_UPDATED", {
adminId: admin.id, adminId: admin.id,
username: admin.username, username: admin.username,
keys: entries.map((e) => e.key), keys: entries.map((e) => e.key),
}, request.ip); });
} }
return reply.send({ ok: true, updatedCount: entries.length }); return reply.send({ ok: true, updatedCount: entries.length });
+11 -16
View File
@@ -471,25 +471,20 @@ export function createToolRoute<T>(app: FastifyInstance, config: ToolRouteConfig
// Fire-and-forget: audit log must never block the response // Fire-and-forget: audit log must never block the response
import("../lib/audit.js") import("../lib/audit.js")
.then(({ isToolAuditEnabled, auditLog }) => .then(({ isToolAuditEnabled, auditFromRequest }) =>
isToolAuditEnabled().then((enabled) => { isToolAuditEnabled().then((enabled) => {
if (!enabled) return; if (!enabled) return;
const user = getAuthUser(request); const user = getAuthUser(request);
return auditLog( return auditFromRequest(request)("TOOL_EXECUTED", {
request.log, userId: user?.id,
"TOOL_EXECUTED", username: user?.username,
{ toolId: config.toolId,
userId: user?.id, inputFileCount: received.length,
username: user?.username, totalInputSize: received.reduce((sum, r) => sum + r.size, 0),
toolId: config.toolId, outputFormat: (settings as Record<string, unknown>)?.format ?? null,
inputFileCount: received.length, status: "success",
totalInputSize: received.reduce((sum, r) => sum + r.size, 0), durationMs: Date.now() - startTime,
outputFormat: (settings as Record<string, unknown>)?.format ?? null, });
status: "success",
durationMs: Date.now() - startTime,
},
request.ip,
);
}), }),
) )
.catch(() => {}); .catch(() => {});
+6 -6
View File
@@ -17,7 +17,7 @@ import sharp from "sharp";
import { z } from "zod"; import { z } from "zod";
import { env } from "../config.js"; import { env } from "../config.js";
import { db, schema } from "../db/index.js"; import { db, schema } from "../db/index.js";
import { auditLog } from "../lib/audit.js"; import { auditFromRequest } from "../lib/audit.js";
import { import {
deleteStoredFile, deleteStoredFile,
deleteThumbnail, deleteThumbnail,
@@ -259,11 +259,11 @@ export async function userFileRoutes(app: FastifyInstance): Promise<void> {
return reply.status(400).send({ error: "No valid files uploaded" }); return reply.status(400).send({ error: "No valid files uploaded" });
} }
await auditLog(request.log, "FILE_UPLOADED", { await auditFromRequest(request)("FILE_UPLOADED", {
userId, userId,
count: created.length, count: created.length,
files: created.map((f) => f.originalName), files: created.map((f) => f.originalName),
}, request.ip); });
return reply.status(201).send({ files: created }); return reply.status(201).send({ files: created });
}, },
@@ -495,7 +495,7 @@ export async function userFileRoutes(app: FastifyInstance): Promise<void> {
.map((f) => f.id); .map((f) => f.id);
if (validIds.length === 0) { if (validIds.length === 0) {
await auditLog(request.log, "FILE_DELETED", { userId: user.id, count: 0, ids }, request.ip); await auditFromRequest(request)("FILE_DELETED", { userId: user.id, count: 0, ids });
return reply.send({ deleted: 0 }); return reply.send({ deleted: 0 });
} }
@@ -542,11 +542,11 @@ export async function userFileRoutes(app: FastifyInstance): Promise<void> {
await db.delete(schema.userFiles).where(inArray(schema.userFiles.id, chainIds)); await db.delete(schema.userFiles).where(inArray(schema.userFiles.id, chainIds));
} }
await auditLog(request.log, "FILE_DELETED", { await auditFromRequest(request)("FILE_DELETED", {
userId: user.id, userId: user.id,
count: chainRows.length, count: chainRows.length,
ids, ids,
}, request.ip); });
return reply.send({ deleted: chainRows.length }); return reply.send({ deleted: chainRows.length });
}); });