Files
pagodera/lib/secure-logger.ts
2025-11-11 03:08:36 -03:00

211 lines
4.7 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Logger Seguro - Conformidade LGPD
* Mascara automaticamente dados pessoais sensíveis em logs
* Previne vazamento de informações em ambientes de produção
*/
interface MaskOptions {
/**
* Mascara parcial de email (mantém parte visível)
* Ex: [email protected] -> u***@domain.com
*/
maskEmail?: boolean;
/**
* Mascara parcial de CPF/CNPJ
* Ex: 123.456.789-00 -> ***.***.***-00
*/
maskDocument?: boolean;
/**
* Mascara telefone completo
* Ex: (11) 98765-4321 -> (11) *****-4321
*/
maskPhone?: boolean;
/**
* Remove completamente dados sensíveis em produção
*/
removeSensitive?: boolean;
}
const _isProduction = process.env.NODE_ENV === "production";
/**
* Mascara email mantendo primeiro caractere e domínio
*/
function maskEmail(email: string): string {
if (!email || !email.includes("@")) return email;
const [localPart, domain] = email.split("@");
const maskedLocal = `${localPart.substring(0, 1)}***`;
return `${maskedLocal}@${domain}`;
}
/**
* Mascara CPF mantendo apenas os dois últimos dígitos
*/
function maskCPF(cpf: string): string {
if (!cpf) return cpf;
// Remove formatação
const cleaned = cpf.replace(/[^\d]/g, "");
if (cleaned.length === 11) {
return `***.***.***-${cleaned.slice(-2)}`;
}
if (cleaned.length === 14) {
// CNPJ
return `**.***.***/****-${cleaned.slice(-2)}`;
}
return "***.***.***-**";
}
/**
* Mascara telefone mantendo DDD e últimos 4 dígitos
*/
function maskPhone(phone: string): string {
if (!phone) return phone;
// Remove formatação
const cleaned = phone.replace(/[^\d]/g, "");
if (cleaned.length >= 10) {
const ddd = cleaned.substring(0, 2);
const lastFour = cleaned.slice(-4);
return `(${ddd}) *****-${lastFour}`;
}
return "**-****-****";
}
/**
* Mascara objeto recursivamente
*/
function maskSensitiveData(obj: any, options: MaskOptions = {}): any {
if (!obj || typeof obj !== "object") {
return obj;
}
// Se for string, verifica se é dado sensível
if (typeof obj === "string") {
// Email
if (obj.includes("@") && options.maskEmail) {
return maskEmail(obj);
}
// CPF/CNPJ
const digitsOnly = obj.replace(/[^\d]/g, "");
if (
(digitsOnly.length === 11 || digitsOnly.length === 14) &&
options.maskDocument
) {
return maskCPF(obj);
}
// Telefone
if (digitsOnly.length >= 10 && options.maskPhone) {
return maskPhone(obj);
}
return obj;
}
// Se for array
if (Array.isArray(obj)) {
return obj.map((item) => maskSensitiveData(item, options));
}
// Se for objeto
const masked: any = {};
for (const [key, value] of Object.entries(obj)) {
const lowerKey = key.toLowerCase();
// Identificar campos sensíveis
const isEmail = lowerKey.includes("email") || lowerKey === "mail";
const isDocument =
lowerKey.includes("cpf") ||
lowerKey.includes("cnpj") ||
lowerKey.includes("taxid");
const isPhone =
lowerKey.includes("phone") ||
lowerKey.includes("cellphone") ||
lowerKey.includes("telefone");
if (isEmail && options.maskEmail) {
masked[key] = maskEmail(String(value));
} else if (isDocument && options.maskDocument) {
masked[key] = maskCPF(String(value));
} else if (isPhone && options.maskPhone) {
masked[key] = maskPhone(String(value));
} else {
masked[key] = maskSensitiveData(value, options);
}
}
return masked;
}
/**
* Logger seguro que mascara dados sensíveis
*/
export const secureLogger = {
/**
* Log info com mascaramento automático
* SEMPRE exibe em produção para debug
*/
info(message: string, data?: any) {
if (data) {
const masked = maskSensitiveData(data, {
maskEmail: true,
maskDocument: true,
maskPhone: true,
});
console.log(`ℹ️ ${message}`, JSON.stringify(masked, null, 2));
} else {
console.log(`ℹ️ ${message}`, "");
}
},
/**
* Log debug com mascaramento automático
* SEMPRE exibe em produção para debug
*/
debug(message: string, data?: any) {
if (data) {
const masked = maskSensitiveData(data, {
maskEmail: true,
maskDocument: true,
maskPhone: true,
});
console.debug(`🔍 ${message}`, JSON.stringify(masked, null, 2));
} else {
console.debug(`🔍 ${message}`, "");
}
},
/**
* Log warning
*/
warn(message: string, data?: any) {
console.warn(`⚠️ ${message}`, data || "");
},
/**
* Log error
*/
error(message: string, error?: any) {
console.error(`❌ ${message}`, error || "");
},
/**
* Mascara dados manualmente
*/
mask(data: any, options?: MaskOptions) {
return maskSensitiveData(data, options);
},
};