import { FastifyInstance, FastifyRequest } from 'fastify'; import { PrismaClient } from '@prisma/client'; import { z } from 'zod'; const prisma = new PrismaClient(); // Helper to extract client info from request function extractClientInfo(request: FastifyRequest) { return { ipAddress: request.ip, userAgent: request.headers['user-agent'] || null, sessionId: (request as any).sessionId || null }; } // Helper to create audit log entry export async function createAuditLog(data: { userId?: string; userName?: string; action: 'CREATE' | 'UPDATE' | 'DELETE' | 'LOGIN' | 'LOGOUT' | 'ACCESS' | 'EXPORT' | 'APPROVE' | 'REJECT'; entity: string; entityId?: string; entityName?: string; before?: any; after?: any; changes?: any; metadata?: any; ipAddress?: string; userAgent?: string; sessionId?: string; }) { try { return await prisma.auditLog.create({ data }); } catch (error) { console.error('Failed to create audit log:', error); return null; } } // Calculate changes between two objects function calculateChanges(before: any, after: any): any { if (!before || !after) return null; const changes: any = {}; const allKeys = new Set([...Object.keys(before), ...Object.keys(after)]); for (const key of allKeys) { if (JSON.stringify(before[key]) !== JSON.stringify(after[key])) { changes[key] = { from: before[key], to: after[key] }; } } return Object.keys(changes).length > 0 ? changes : null; } export async function auditRoutes(fastify: FastifyInstance) { // Auth middleware fastify.addHook('onRequest', async (request) => { try { await request.jwtVerify(); } catch (err) { throw err; } }); /** * GET /audit/logs * Query audit logs with filters */ fastify.get('/logs', { handler: async (request, reply) => { try { const { userId, entity, entityId, action, startDate, endDate, page = 1, limit = 50 } = request.query as any; const where: any = {}; if (userId) where.userId = userId; if (entity) where.entity = entity; if (entityId) where.entityId = entityId; if (action) where.action = action; if (startDate || endDate) { where.timestamp = {}; if (startDate) where.timestamp.gte = new Date(startDate); if (endDate) where.timestamp.lte = new Date(endDate); } const [logs, total] = await Promise.all([ prisma.auditLog.findMany({ where, orderBy: { timestamp: 'desc' }, skip: (page - 1) * limit, take: parseInt(limit) }), prisma.auditLog.count({ where }) ]); return { logs, pagination: { page: parseInt(page), limit: parseInt(limit), total, pages: Math.ceil(total / limit) } }; } catch (error) { fastify.log.error(error); return reply.status(500).send({ error: 'Failed to fetch audit logs' }); } } }); /** * GET /audit/logs/:id * Get single audit log entry with full details */ fastify.get('/logs/:id', { handler: async (request, reply) => { try { const { id } = request.params as any; const log = await prisma.auditLog.findUnique({ where: { id } }); if (!log) { return reply.status(404).send({ error: 'Log not found' }); } return log; } catch (error) { fastify.log.error(error); return reply.status(500).send({ error: 'Failed to fetch log' }); } } }); /** * GET /audit/entity/:entity/:entityId * Get all audit logs for a specific entity */ fastify.get('/entity/:entity/:entityId', { handler: async (request, reply) => { try { const { entity, entityId } = request.params as any; const logs = await prisma.auditLog.findMany({ where: { entity, entityId }, orderBy: { timestamp: 'desc' } }); return { entity, entityId, history: logs }; } catch (error) { fastify.log.error(error); return reply.status(500).send({ error: 'Failed to fetch entity history' }); } } }); /** * GET /audit/user/:userId * Get all audit logs for a specific user */ fastify.get('/user/:userId', { handler: async (request, reply) => { try { const { userId } = request.params as any; const { limit = 100 } = request.query as any; const logs = await prisma.auditLog.findMany({ where: { userId }, orderBy: { timestamp: 'desc' }, take: parseInt(limit) }); return { userId, activityLog: logs }; } catch (error) { fastify.log.error(error); return reply.status(500).send({ error: 'Failed to fetch user activity' }); } } }); /** * GET /audit/summary * Get audit log summary/statistics */ fastify.get('/summary', { handler: async (request, reply) => { try { const { startDate, endDate } = request.query as any; const where: any = {}; if (startDate || endDate) { where.timestamp = {}; if (startDate) where.timestamp.gte = new Date(startDate); if (endDate) where.timestamp.lte = new Date(endDate); } const [totalLogs, actionCounts, entityCounts, recentActivity] = await Promise.all([ prisma.auditLog.count({ where }), prisma.auditLog.groupBy({ by: ['action'], where, _count: true }), prisma.auditLog.groupBy({ by: ['entity'], where, _count: true, orderBy: { _count: { entity: 'desc' } }, take: 10 }), prisma.auditLog.findMany({ where, orderBy: { timestamp: 'desc' }, take: 10, select: { id: true, action: true, entity: true, entityName: true, userName: true, timestamp: true } }) ]); return { totalLogs, byAction: actionCounts.reduce((acc: any, item) => { acc[item.action] = item._count; return acc; }, {}), byEntity: entityCounts.map(e => ({ entity: e.entity, count: e._count })), recentActivity }; } catch (error) { fastify.log.error(error); return reply.status(500).send({ error: 'Failed to fetch summary' }); } } }); /** * GET /audit/export * Export audit logs as CSV */ fastify.get('/export', { handler: async (request, reply) => { try { const user = request.user as any; const { entity, startDate, endDate } = request.query as any; const where: any = {}; if (entity) where.entity = entity; if (startDate || endDate) { where.timestamp = {}; if (startDate) where.timestamp.gte = new Date(startDate); if (endDate) where.timestamp.lte = new Date(endDate); } const logs = await prisma.auditLog.findMany({ where, orderBy: { timestamp: 'desc' }, take: 10000 // Limit export size }); // Log the export action await createAuditLog({ userId: user.id, userName: user.name || user.email, action: 'EXPORT', entity: 'AuditLog', metadata: { count: logs.length, filters: { entity, startDate, endDate } }, ...extractClientInfo(request) }); // Generate CSV const headers = ['Timestamp', 'User', 'Action', 'Entity', 'Entity Name', 'Entity ID', 'IP Address']; const rows = logs.map(log => [ log.timestamp.toISOString(), log.userName || log.userId || 'System', log.action, log.entity, log.entityName || '', log.entityId || '', log.ipAddress || '' ]); const csv = [ headers.join(','), ...rows.map(r => r.map(c => `"${c}"`).join(',')) ].join('\n'); reply.header('Content-Type', 'text/csv'); reply.header('Content-Disposition', `attachment; filename="audit-log-${new Date().toISOString().split('T')[0]}.csv"`); return csv; } catch (error) { fastify.log.error(error); return reply.status(500).send({ error: 'Failed to export' }); } } }); } // Export helper for use in other routes export { createAuditLog as logAuditEvent, calculateChanges };