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), } }