Add Didar gap features: lost-reason reports, audit logs, tags, card assignees, and saved-views API.
Ignore Graphify local artifacts and expose new modules in superadmin.
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
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,
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user