import { CallHandler, ExecutionContext, Injectable, NestInterceptor, } from '@nestjs/common'; import type { Request } from 'express'; import { Observable, tap } from 'rxjs'; import { auditStory } from './audit-detail'; 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 { const http = context.switchToHttp(); const req = http.getRequest(); 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 ip = (req.headers['x-forwarded-for'] as string | undefined)?.split(',')[0]?.trim() || req.ip || null; return next.handle().pipe( tap({ next: (response) => { const story = auditStory(method, req.body, response); const summary = story.summary || `${action} ${entityType ?? 'منبع'} از مسیر ${path.split('?')[0]}`; void this.audit.write({ organizationId, actorUserId, action, method, path: path.split('?')[0] ?? path, entityType, entityId, summary, meta: story.label || story.detail ? { label: story.label, detail: story.detail } : null, ip, }); }, }), ); } }