Dealer_Onboarding_Backend/src/services/systemAuditLog.service.ts

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