/** * @ai-summary Fastify route handlers for audit log API * @ai-context HTTP request/response handling for audit log search and export */ import { FastifyRequest, FastifyReply } from 'fastify'; import { AuditLogService } from '../domain/audit-log.service'; import { AuditLogRepository } from '../data/audit-log.repository'; import { AuditLogFilters, isValidCategory, isValidSeverity } from '../domain/audit-log.types'; import { pool } from '../../../core/config/database'; import { logger } from '../../../core/logging/logger'; interface AuditLogsQuery { search?: string; category?: string; severity?: string; startDate?: string; endDate?: string; limit?: string; offset?: string; } export class AuditLogController { private service: AuditLogService; constructor() { const repository = new AuditLogRepository(pool); this.service = new AuditLogService(repository); } /** * GET /api/admin/audit-logs - Search audit logs with filters */ async getAuditLogs( request: FastifyRequest<{ Querystring: AuditLogsQuery }>, reply: FastifyReply ) { try { const { search, category, severity, startDate, endDate, limit, offset } = request.query; // Validate category if provided if (category && !isValidCategory(category)) { return reply.code(400).send({ error: 'Bad Request', message: `Invalid category: ${category}. Valid values: auth, vehicle, user, system, admin`, }); } // Validate severity if provided if (severity && !isValidSeverity(severity)) { return reply.code(400).send({ error: 'Bad Request', message: `Invalid severity: ${severity}. Valid values: info, warning, error`, }); } const filters: AuditLogFilters = { search, category: category as AuditLogFilters['category'], severity: severity as AuditLogFilters['severity'], startDate: startDate ? new Date(startDate) : undefined, endDate: endDate ? new Date(endDate) : undefined, }; const pagination = { limit: Math.min(parseInt(limit || '50', 10), 100), offset: parseInt(offset || '0', 10), }; const result = await this.service.search(filters, pagination); return reply.send(result); } catch (error) { logger.error('Error fetching audit logs', { error }); return reply.code(500).send({ error: 'Internal Server Error', message: 'Failed to fetch audit logs', }); } } /** * GET /api/admin/audit-logs/export - Export audit logs as CSV */ async exportAuditLogs( request: FastifyRequest<{ Querystring: AuditLogsQuery }>, reply: FastifyReply ) { try { const { search, category, severity, startDate, endDate } = request.query; // Validate category if provided if (category && !isValidCategory(category)) { return reply.code(400).send({ error: 'Bad Request', message: `Invalid category: ${category}`, }); } // Validate severity if provided if (severity && !isValidSeverity(severity)) { return reply.code(400).send({ error: 'Bad Request', message: `Invalid severity: ${severity}`, }); } const filters: AuditLogFilters = { search, category: category as AuditLogFilters['category'], severity: severity as AuditLogFilters['severity'], startDate: startDate ? new Date(startDate) : undefined, endDate: endDate ? new Date(endDate) : undefined, }; const { logs, truncated } = await this.service.getForExport(filters); // Generate CSV const headers = ['ID', 'Timestamp', 'Category', 'Severity', 'User ID', 'Action', 'Resource Type', 'Resource ID']; const rows = logs.map((log) => [ log.id, log.createdAt.toISOString(), log.category, log.severity, log.userId || '', `"${log.action.replace(/"/g, '""')}"`, // Escape quotes in CSV log.resourceType || '', log.resourceId || '', ]); const csv = [headers.join(','), ...rows.map((row) => row.join(','))].join('\n'); // Set headers for file download const filename = `audit-logs-${new Date().toISOString().split('T')[0]}.csv`; reply.header('Content-Type', 'text/csv'); reply.header('Content-Disposition', `attachment; filename="${filename}"`); // Warn if results were truncated if (truncated) { reply.header('X-Export-Truncated', 'true'); reply.header('X-Export-Limit', '5000'); logger.warn('Audit log export was truncated', { exportedCount: logs.length }); } return reply.send(csv); } catch (error) { logger.error('Error exporting audit logs', { error }); return reply.code(500).send({ error: 'Internal Server Error', message: 'Failed to export audit logs', }); } } }