109 lines
3.7 KiB
TypeScript
109 lines
3.7 KiB
TypeScript
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<string, unknown> | null;
|
|
newData?: Record<string, unknown> | null;
|
|
metadata?: Record<string, unknown> | 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<void> {
|
|
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<Pick<SystemAuditPayload, 'userId' | 'actorName' | 'actorRole'>>
|
|
): Promise<void> {
|
|
const ctx = extractRequestContext(req);
|
|
await safeSystemAuditLogCreate({
|
|
...ctx,
|
|
...payload
|
|
});
|
|
}
|