forked from Yara724/api
fanavaran duplication request problems fixed.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user