fanavaran duplication request problems fixed.

This commit is contained in:
2026-08-02 17:00:19 +03:30
parent c2f5c576fa
commit b345818d43
9 changed files with 884 additions and 123 deletions

View File

@@ -1,6 +1,6 @@
import { Injectable, Logger } from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose";
import { isAxiosError } from "axios";
import { isAxiosError, type AxiosResponse } from "axios";
import { randomBytes } from "node:crypto";
import { Model, Types } from "mongoose";
import {
@@ -16,18 +16,54 @@ export interface RecordFanavaranAuditStepInput {
step: FanavaranAuditStep;
status: FanavaranAuditStatus;
requestUrl?: string;
requestMethod?: string;
httpStatus?: number;
requestHeaders?: Record<string, unknown>;
requestBody?: unknown;
requestMeta?: Record<string, unknown>;
responseHeaders?: Record<string, unknown>;
responseBody?: unknown;
responseMeta?: Record<string, unknown>;
errorMessage?: string;
errorDetails?: Record<string, unknown>;
durationMs?: number;
}
export interface FanavaranHttpExchange {
httpStatus?: number;
requestHeaders?: Record<string, unknown>;
responseHeaders?: Record<string, unknown>;
responseBody?: unknown;
}
@Injectable()
export class FanavaranAuditService {
private readonly logger = new Logger(FanavaranAuditService.name);
/** Max serialized chars kept for request/response bodies in audit docs. */
static readonly BODY_MAX_CHARS = 80_000;
private static readonly SENSITIVE_HEADER_KEYS = new Set([
"password",
"secret",
"authorization",
"authenticationtoken",
"apptoken",
"app-token",
"x-api-key",
"cookie",
"set-cookie",
]);
private static readonly SENSITIVE_BODY_KEYS = new Set([
"password",
"secret",
"authenticationtoken",
"apptoken",
"token",
"files",
]);
constructor(
@InjectModel(FanavaranAuditLog.name)
private readonly auditModel: Model<FanavaranAuditLogDocument>,
@@ -47,6 +83,138 @@ export class FanavaranAuditService {
return `${trimmed.slice(0, 3)}****${trimmed.slice(-2)}`;
}
maskToken(value: string): string {
const trimmed = value.trim();
if (trimmed.length <= 8) {
return "****";
}
return `${trimmed.slice(0, 4)}…${trimmed.slice(-4)} (len=${trimmed.length})`;
}
sanitizeHeaders(
headers?: Record<string, unknown> | null,
): Record<string, unknown> | undefined {
if (!headers || typeof headers !== "object") {
return undefined;
}
const out: Record<string, unknown> = {};
for (const [rawKey, rawValue] of Object.entries(headers)) {
if (rawValue === undefined) continue;
const key = String(rawKey);
const lower = key.toLowerCase();
if (FanavaranAuditService.SENSITIVE_HEADER_KEYS.has(lower)) {
out[key] =
typeof rawValue === "string"
? this.maskToken(rawValue)
: "***";
continue;
}
if (
typeof rawValue === "string" ||
typeof rawValue === "number" ||
typeof rawValue === "boolean" ||
rawValue === null
) {
out[key] = rawValue;
} else {
out[key] = String(rawValue);
}
}
return out;
}
sanitizeBody(body: unknown): unknown {
if (body === undefined) {
return undefined;
}
const masked = this.maskSensitiveDeep(body);
try {
const serialized = JSON.stringify(masked);
if (serialized.length <= FanavaranAuditService.BODY_MAX_CHARS) {
return masked;
}
return {
_truncated: true,
maxChars: FanavaranAuditService.BODY_MAX_CHARS,
preview: serialized.slice(0, FanavaranAuditService.BODY_MAX_CHARS),
};
} catch {
const asString = String(masked);
return asString.length <= FanavaranAuditService.BODY_MAX_CHARS
? asString
: {
_truncated: true,
preview: asString.slice(0, FanavaranAuditService.BODY_MAX_CHARS),
};
}
}
/**
* Extract request/response headers + body from an Axios response or Axios
* error (uses `config.headers` when present). Values are returned raw;
* `recordStep` applies sanitisation before persistence.
*/
captureAxiosExchange(
source: unknown,
overrideRequestHeaders?: Record<string, unknown>,
): FanavaranHttpExchange {
const toPlainHeaders = (
headers?: Record<string, unknown> | null,
): Record<string, unknown> | undefined => {
if (!headers || typeof headers !== "object") return undefined;
const out: Record<string, unknown> = {};
for (const [key, value] of Object.entries(headers)) {
if (value === undefined) continue;
out[key] =
typeof value === "string" ||
typeof value === "number" ||
typeof value === "boolean" ||
value === null
? value
: String(value);
}
return out;
};
if (isAxiosError(source)) {
return {
httpStatus: source.response?.status,
requestHeaders: toPlainHeaders(
overrideRequestHeaders ??
(source.config?.headers as Record<string, unknown> | undefined),
),
responseHeaders: toPlainHeaders(
source.response?.headers as Record<string, unknown> | undefined,
),
responseBody: source.response?.data,
};
}
const response = source as AxiosResponse | null;
if (
response &&
typeof response === "object" &&
"status" in response &&
"headers" in response
) {
return {
httpStatus: response.status,
requestHeaders: toPlainHeaders(
overrideRequestHeaders ??
(response.config?.headers as Record<string, unknown> | undefined),
),
responseHeaders: toPlainHeaders(
response.headers as Record<string, unknown> | undefined,
),
responseBody: response.data,
};
}
return {
requestHeaders: toPlainHeaders(overrideRequestHeaders),
};
}
sanitizeErrorDetails(error: unknown): Record<string, unknown> {
if (isAxiosError(error)) {
const data = error.response?.data;
@@ -54,12 +222,13 @@ export class FanavaranAuditService {
type: "axios",
status: error.response?.status,
statusText: error.response?.statusText,
data:
data: this.sanitizeBody(
typeof data === "object" && data !== null
? data
: typeof data === "string"
? data.slice(0, 2000)
: data,
),
};
}
if (error instanceof Error) {
@@ -112,8 +281,13 @@ export class FanavaranAuditService {
? { claimRequestId: new Types.ObjectId(input.session.claimRequestId) }
: {}),
requestUrl: input.requestUrl,
requestMethod: input.requestMethod,
httpStatus: input.httpStatus,
requestHeaders: this.sanitizeHeaders(input.requestHeaders),
requestBody: this.sanitizeBody(input.requestBody),
requestMeta: input.requestMeta,
responseHeaders: this.sanitizeHeaders(input.responseHeaders),
responseBody: this.sanitizeBody(input.responseBody),
responseMeta: input.responseMeta,
errorMessage: input.errorMessage,
errorDetails: input.errorDetails,
@@ -134,4 +308,31 @@ export class FanavaranAuditService {
.lean()
.exec();
}
private maskSensitiveDeep(value: unknown, depth = 0): unknown {
if (depth > 8) {
return "[max-depth]";
}
if (value === null || value === undefined) {
return value;
}
if (Array.isArray(value)) {
return value.map((item) => this.maskSensitiveDeep(item, depth + 1));
}
if (typeof value === "object") {
const out: Record<string, unknown> = {};
for (const [key, child] of Object.entries(
value as Record<string, unknown>,
)) {
if (FanavaranAuditService.SENSITIVE_BODY_KEYS.has(key.toLowerCase())) {
out[key] =
typeof child === "string" ? this.maskToken(child) : "[redacted]";
continue;
}
out[key] = this.maskSensitiveDeep(child, depth + 1);
}
return out;
}
return value;
}
}