- Global exception filter with consistent error response format - Domain exceptions (NotFoundException, ValidationException, etc.) - Error codes enum for domain-specific error identification - Correlation ID middleware for request tracing - Request/response logging middleware with structured JSON - PII masking in logs (emails, phone numbers, sensitive fields) - Enhanced LoggerService with pino formatters and ISO timestamps - Tests for exception filter, domain exceptions, and PII masker Co-Authored-By: Paperclip <noreply@paperclip.ing>
54 lines
1.3 KiB
TypeScript
54 lines
1.3 KiB
TypeScript
/**
|
|
* PII masking utility for structured logs.
|
|
* Redacts sensitive fields in log data before they reach the transport.
|
|
*/
|
|
|
|
const SENSITIVE_KEYS = new Set([
|
|
'password',
|
|
'token',
|
|
'accessToken',
|
|
'refreshToken',
|
|
'secret',
|
|
'authorization',
|
|
'cookie',
|
|
'creditCard',
|
|
'cardNumber',
|
|
'cvv',
|
|
'ssn',
|
|
'cmnd',
|
|
'cccd',
|
|
]);
|
|
|
|
const EMAIL_REGEX = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
|
|
const PHONE_REGEX = /(?:\+84|0)\d{9,10}/g;
|
|
|
|
export function maskPii(obj: unknown): unknown {
|
|
if (obj === null || obj === undefined) return obj;
|
|
if (typeof obj === 'string') return maskString(obj);
|
|
if (Array.isArray(obj)) return obj.map(maskPii);
|
|
if (typeof obj === 'object') {
|
|
const masked: Record<string, unknown> = {};
|
|
for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
|
|
if (SENSITIVE_KEYS.has(key)) {
|
|
masked[key] = '[REDACTED]';
|
|
} else {
|
|
masked[key] = maskPii(value);
|
|
}
|
|
}
|
|
return masked;
|
|
}
|
|
return obj;
|
|
}
|
|
|
|
function maskString(value: string): string {
|
|
let result = value;
|
|
result = result.replace(EMAIL_REGEX, (match) => {
|
|
const [local, domain] = match.split('@');
|
|
return `${local![0]}***@${domain}`;
|
|
});
|
|
result = result.replace(PHONE_REGEX, (match) => {
|
|
return match.slice(0, 3) + '****' + match.slice(-3);
|
|
});
|
|
return result;
|
|
}
|