Merge pull request 'feat: implement global HTTP request logging and audit interception for entity creation' (#34) from waseem into development

Reviewed-on: https://gitea.maskantech.in/gitea_admin/aeroresolve_backend/pulls/34
This commit is contained in:
Syed Waseem khadri Rafai
2026-09-10 06:44:32 +00:00
3 changed files with 89 additions and 13 deletions
+2
View File
@@ -9,6 +9,7 @@ import { MasterDataModule } from './modules/master-data/master-data.module';
import { CohortModule } from './modules/cohort/cohort.module';
import { TenantModule } from './modules/tenant/tenant.module';
import { TenantMiddleware } from './common/tenant/tenant.middleware';
import { HttpLoggerMiddleware } from './common/middleware/http-logger.middleware';
import { PolicyEngineModule } from './modules/policy-engine/policy-engine.module';
import { RecoveryIncidentModule } from './modules/recovery-incident/recovery-incident.module';
import { AuditLogModule } from './modules/audit-log/audit-log.module';
@@ -48,6 +49,7 @@ const envFilePath = env ? [`.env.${env}`, '.env.local', '.env'] : ['.env.local',
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(HttpLoggerMiddleware).forRoutes('*');
consumer.apply(TenantMiddleware).forRoutes('*');
}
}
+45 -13
View File
@@ -23,11 +23,14 @@ const MODULE_MAP: Array<{ pattern: RegExp; module: string }> = [
{ pattern: /master-data/, module: 'master-data' },
];
function resolveModule(url: string): string {
// Routes that should never trigger an entity creation audit log
const EXCLUDED_PATTERNS: RegExp[] = [/\/auth\b/, /\/evaluate\b/];
function resolveModule(url: string): string | null {
for (const entry of MODULE_MAP) {
if (entry.pattern.test(url)) return entry.module;
}
return 'unknown';
return null;
}
function resolveAction(method: string, url: string): string {
@@ -54,33 +57,62 @@ export class AuditInterceptor implements NestInterceptor {
return next.handle();
}
// Never audit authentication endpoints or non-persisting evaluation endpoints
if (EXCLUDED_PATTERNS.some((pattern) => pattern.test(url))) {
return next.handle();
}
const module = resolveModule(url);
if (!module) {
return next.handle();
}
const action = resolveAction(method, url);
const userAgent = headers['user-agent'] ?? '';
const tenantId = (headers['x-tenant-id'] as string) ?? 'unknown';
this.logger.debug(`Intercepted ${method} ${url} -> Module: ${module}, Action: ${action}`);
this.logger.debug(
`Intercepted ${method} ${url} -> Module: ${module}, Action: ${action}`,
);
return next.handle().pipe(
tap({
next: async (responseData: any) => {
next: (responseData: unknown) => {
this.logger.debug(`Writing audit log for ${module} ${action}...`);
try {
await this.auditLogService.log({
const resObj =
typeof responseData === 'object' && responseData !== null
? (responseData as Record<string, unknown>)
: undefined;
const entityId =
typeof resObj?.id === 'string' ? resObj.id : undefined;
const entityLabel =
typeof resObj?.name === 'string'
? resObj.name
: typeof resObj?.recoveryCode === 'string'
? resObj.recoveryCode
: typeof resObj?.flightNumber === 'string'
? resObj.flightNumber
: undefined;
void this.auditLogService
.log({
module,
action,
entityId: responseData?.id ?? undefined,
entityLabel: responseData?.name ?? responseData?.recoveryCode ?? responseData?.flightNumber ?? undefined,
entityId,
entityLabel,
before: undefined, // no before on CREATE
after: responseData ?? undefined,
after: resObj as Record<string, any> | undefined,
performedBy: tenantId,
ipAddress: ip,
userAgent: String(userAgent),
})
.catch((err: unknown) => {
// Never let audit failure break the main request
this.logger.warn(
`Audit log failed for ${method} ${url}: ${String(err)}`,
);
});
} catch (err) {
// Never let audit failure break the main request
this.logger.warn(`Audit log failed for ${method} ${url}: ${err}`);
}
},
}),
);
@@ -0,0 +1,42 @@
import { Injectable, Logger, NestMiddleware } from '@nestjs/common';
import { NextFunction, Request, Response } from 'express';
@Injectable()
export class HttpLoggerMiddleware implements NestMiddleware {
private readonly logger = new Logger('HTTP');
use(req: Request, res: Response, next: NextFunction): void {
const { method, originalUrl, ip } = req;
const startTime = Date.now();
res.on('finish', () => {
const { statusCode } = res;
const duration = Date.now() - startTime;
const rawTenant = req.headers['x-tenant-id'];
const tenantHeader = Array.isArray(rawTenant) ? rawTenant[0] : rawTenant;
const tenantInfo = tenantHeader ? ` [tenant: ${tenantHeader}]` : '';
const clientIp = typeof ip === 'string' ? ip : 'unknown';
const message = `${method} ${originalUrl} ${statusCode} +${duration}ms - ${clientIp}${tenantInfo}`;
if (statusCode >= 500) {
this.logger.error(message);
} else if (statusCode >= 400) {
this.logger.warn(message);
} else {
this.logger.log(message);
}
});
res.on('close', () => {
if (!res.writableEnded) {
const duration = Date.now() - startTime;
const clientIp = typeof ip === 'string' ? ip : 'unknown';
this.logger.warn(
`${method} ${originalUrl} CLOSED_PREMATURELY +${duration}ms - ${clientIp}`,
);
}
});
next();
}
}