211 lines
4.7 KiB
TypeScript
211 lines
4.7 KiB
TypeScript
/**
|
||
* 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);
|
||
},
|
||
};
|