import { prisma } from "../prisma"; interface AuditEntry { userId: number; action: string; target: string; targetId?: number; before?: Record; after?: Record; } const SENSITIVE_KEY_RE = /password|secret|token|otp|recovery|authTicket|two_factor|two_factor_secret|api_key/i; const REDACTED = "[Redacted]"; function sanitizeAuditPayload(value: unknown, depth = 0): unknown { if (depth > 6 || value == null) return value; if (Array.isArray(value)) return value.map((v) => sanitizeAuditPayload(v, depth + 1)); if (typeof value !== "object") return value; const out: Record = {}; for (const [key, val] of Object.entries(value as Record)) { out[key] = SENSITIVE_KEY_RE.test(key) ? REDACTED : sanitizeAuditPayload(val, depth + 1); } return out; } function computeDiff( before?: Record, after?: Record, ): Record | null { if (!before || !after) return null; const diff: Record = {}; const allKeys = new Set([...Object.keys(before), ...Object.keys(after)]); for (const key of allKeys) { if (JSON.stringify(before[key]) !== JSON.stringify(after[key])) { diff[key] = { from: before[key], to: after[key] }; } } return Object.keys(diff).length > 0 ? diff : null; } export async function logAudit(entry: AuditEntry): Promise { const sanitizedBefore = entry.before ? (sanitizeAuditPayload(entry.before) as Record) : undefined; const sanitizedAfter = entry.after ? (sanitizeAuditPayload(entry.after) as Record) : undefined; const diff = computeDiff(sanitizedBefore, sanitizedAfter); await prisma.adminAuditLog.create({ data: { userId: entry.userId, action: entry.action, target: entry.target, targetId: entry.targetId, before: sanitizedBefore ? JSON.stringify(sanitizedBefore) : null, after: sanitizedAfter ? JSON.stringify(sanitizedAfter) : null, diff: diff ? JSON.stringify(diff) : null, createdAt: new Date().toISOString(), }, }); } interface GetLogsOptions { search?: string; page?: number; perPage?: number; } export async function getAuditLogs(options: GetLogsOptions = {}) { const { search, page = 1, perPage = 20 } = options; const skip = (page - 1) * perPage; const where = search ? { OR: [ { action: { contains: search } }, { target: { contains: search } }, ], } : {}; const [rows, total] = await Promise.all([ prisma.adminAuditLog.findMany({ where, orderBy: { id: "desc" }, skip, take: perPage, }), prisma.adminAuditLog.count({ where }), ]); const userIds = [...new Set(rows.map((r) => r.userId))]; const users = await prisma.user.findMany({ where: { id: { in: userIds } }, select: { id: true, username: true }, }); const userMap = new Map(users.map((u) => [u.id, u.username])); const enrichedRows = rows.map((r) => ({ ...r, username: userMap.get(r.userId) ?? `User #${r.userId}`, })); return { rows: enrichedRows, total, page, perPage, lastPage: Math.ceil(total / perPage), }; }