refactor(api): enhance donation creation with rate limiting, Zod validation, and secure logging

- Add rate limiting to prevent abuse on /api/donations/create endpoint
- Replace manual input validation with Zod schema for better error handling and type safety
- Switch from console.log to secureLogger to comply with LGPD and avoid exposing sensitive data
- Remove sensitive request body logging in AbacatePay client for improved security
This commit is contained in:
Evandro Fonseca Junior committed 2025-10-26 23:01:19 -03:00
1 parent bb6b320d1f
commit a1800227a7
11 files changed
+876 -138

No files matched your search

+208
View File
@@ -0,0 +1,208 @@
/**
* 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
*/
info(message: string, data?: any) {
if (isProduction && data) {
const masked = maskSensitiveData(data, {
maskEmail: true,
maskDocument: true,
maskPhone: true,
});
console.log(`ℹ️ ${message}`, JSON.stringify(masked, null, 2));
} else {
console.log(`ℹ️ ${message}`, data ? JSON.stringify(data, null, 2) : "");
}
},
/**
* Log debug com mascaramento automático
*/
debug(message: string, data?: any) {
if (isProduction && data) {
const masked = maskSensitiveData(data, {
maskEmail: true,
maskDocument: true,
maskPhone: true,
});
console.debug(`🔍 ${message}`, JSON.stringify(masked, null, 2));
} else {
console.debug(`🔍 ${message}`, data ? JSON.stringify(data, null, 2) : "");
}
},
/**
* 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);
},
};