import type { Request } from 'express'; import db from '../database/models/index.js'; import type { AuthRequest } from '../types/express.types.js'; const { SystemAuditLog } = db; export type SystemAuditPayload = { userId?: string | null; actorName?: string | null; actorRole?: string | null; module: string; entityType: string; entityId?: string | null; entityLabel?: string | null; action: string; description?: string | null; oldData?: Record | null; newData?: Record | null; metadata?: Record | null; ipAddress?: string | null; userAgent?: string | null; }; export type RequestContext = { userId: string | null; actorName: string | null; actorRole: string | null; ipAddress: string | null; userAgent: string | null; }; /** * Snapshot the actor + transport context from an Express request so the * caller doesn't have to repeat the same property plucking every time. * Returns nulls for unauthenticated requests (e.g. public questionnaire). */ export function extractRequestContext(req: Request | AuthRequest | undefined): RequestContext { const r: any = req || {}; const user = r.user || {}; const ipAddress = (r.headers?.['x-forwarded-for'] as string | undefined)?.split(',')[0]?.trim() || r.ip || r.connection?.remoteAddress || null; const userAgent = (r.get && typeof r.get === 'function' && r.get('user-agent')) || r.headers?.['user-agent'] || null; return { userId: user.id || null, actorName: user.fullName || user.firstName || null, actorRole: user.roleCode || user.role || null, ipAddress: ipAddress ? String(ipAddress).slice(0, 64) : null, userAgent: userAgent ? String(userAgent).slice(0, 500) : null }; } /** * Writes a system-audit row. Never throws — system/config writes must not * break the workflow that triggered them. Failures are logged to stdout/err * so they show up in the Winston combined log. */ export async function safeSystemAuditLogCreate(payload: SystemAuditPayload): Promise { try { await SystemAuditLog.create({ userId: payload.userId ?? null, actorName: payload.actorName ?? null, actorRole: payload.actorRole ?? null, module: payload.module, entityType: payload.entityType, entityId: payload.entityId ? String(payload.entityId) : null, entityLabel: payload.entityLabel ?? null, action: payload.action, description: payload.description ?? null, oldData: payload.oldData ?? null, newData: payload.newData ?? null, metadata: payload.metadata ?? null, ipAddress: payload.ipAddress ?? null, userAgent: payload.userAgent ?? null } as any); } catch (err) { console.error( '[safeSystemAuditLogCreate] Non-fatal system-audit failure:', (err as Error)?.message || err ); } } /** * Convenience wrapper that pulls the actor + transport context from `req` * and merges it into the payload. Caller still supplies module / action / * entity fields and any old/new data they want recorded. */ export async function logSystemAudit( req: Request | AuthRequest | undefined, payload: Omit< SystemAuditPayload, 'userId' | 'actorName' | 'actorRole' | 'ipAddress' | 'userAgent' > & Partial> ): Promise { const ctx = extractRequestContext(req); await safeSystemAuditLogCreate({ ...ctx, ...payload }); }