Files
CRM_Montzar/apps/api/src/audit-logs/audit-logs.interceptor.ts
T

101 lines
2.7 KiB
TypeScript
Raw Normal View History

import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import type { Request } from 'express';
import { Observable, tap } from 'rxjs';
import { AuditLogsService } from './audit-logs.service';
const SKIP_PREFIXES = [
'/api/health',
'/api/auth/login',
'/api/auth/register',
'/api/auth/otp',
'/api/auth/refresh',
'/api/portal',
'/api/public',
'/api/platform',
'/api/audit-logs',
];
type AuthedRequest = Request & {
user?: {
userId?: string;
id?: string;
organizationId?: string;
};
};
function inferEntity(path: string): { entityType: string | null; entityId: string | null } {
const clean = path.replace(/^\/api\/?/, '').split('?')[0] ?? '';
const parts = clean.split('/').filter(Boolean);
if (!parts.length) return { entityType: null, entityId: null };
const entityType = parts[0] ?? null;
const maybeId = parts[1];
const entityId =
maybeId && !['new', 'me', 'summary', 'dashboard', 'board'].includes(maybeId)
? maybeId
: null;
return { entityType, entityId };
}
function actionLabel(method: string, path: string): string {
const m = method.toUpperCase();
if (m === 'POST') return 'ایجاد';
if (m === 'PATCH' || m === 'PUT') return 'ویرایش';
if (m === 'DELETE') return 'حذف';
return m;
void path;
}
@Injectable()
export class AuditLogsInterceptor implements NestInterceptor {
constructor(private readonly audit: AuditLogsService) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const http = context.switchToHttp();
const req = http.getRequest<AuthedRequest>();
const method = (req.method ?? 'GET').toUpperCase();
if (!['POST', 'PATCH', 'PUT', 'DELETE'].includes(method)) {
return next.handle();
}
const path = req.originalUrl || req.url || '';
if (SKIP_PREFIXES.some((p) => path.startsWith(p))) {
return next.handle();
}
const organizationId = req.user?.organizationId;
if (!organizationId) return next.handle();
const actorUserId = req.user?.userId ?? req.user?.id ?? null;
const { entityType, entityId } = inferEntity(path);
const action = actionLabel(method, path);
const summary = `${action} ${entityType ?? 'منبع'} از مسیر ${path.split('?')[0]}`;
const ip =
(req.headers['x-forwarded-for'] as string | undefined)?.split(',')[0]?.trim() ||
req.ip ||
null;
return next.handle().pipe(
tap({
next: () => {
void this.audit.write({
organizationId,
actorUserId,
action,
method,
path: path.split('?')[0] ?? path,
entityType,
entityId,
summary,
ip,
});
},
}),
);
}
}