ca-grow-ops-manager/backend/src/routes/audit.routes.ts
fullsizemalt 32fd739ccf
Some checks failed
Deploy to Production / deploy (push) Failing after 0s
Test / backend-test (push) Failing after 0s
Test / frontend-test (push) Failing after 0s
feat: Complete Phases 8-13 implementation
Phase 8: Visitor Management
- Visitor/VisitorLog/AccessZone models
- Check-in/out with badge generation
- Zone occupancy tracking
- Kiosk and management pages

Phase 9: Messaging & Communication
- Announcements with priority levels
- Acknowledgement tracking
- Shift notes for team handoffs
- AnnouncementBanner component

Phase 10: Compliance & Audit Trail
- Immutable AuditLog model
- Document versioning and approval workflow
- Acknowledgement tracking for SOPs
- CSV export for audit logs

Phase 11: Accessibility & i18n
- WCAG 2.1 AA compliance utilities
- react-i18next with EN/ES translations
- User preferences context (theme, font size, etc)
- High contrast and reduced motion support

Phase 12: Hardware Integration
- QR code generation for batches/plants/visitors
- Printable label system
- Visitor badge printing

Phase 13: Advanced Features
- Environmental monitoring (sensors, readings, alerts)
- Financial tracking (transactions, P&L reports)
- AI/ML insights (yield predictions, anomaly detection)
2025-12-11 00:26:25 -08:00

320 lines
10 KiB
TypeScript

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 };