mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat: add request correlation IDs to audit logs and response headers
This commit is contained in:
@@ -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");
|
||||||
|
|||||||
@@ -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_") ||
|
||||||
|
|||||||
@@ -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 });
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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, {
|
||||||
|
|||||||
@@ -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,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 });
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 });
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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(() => {});
|
||||||
|
|||||||
@@ -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 });
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user