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:
1 parent
bb6b320d1f
commit
a1800227a7
11 files changed
+876
-138
No files matched your search
@@ -0,0 +1,145 @@
|
||||
import { z } from "zod";
|
||||
import validator from "validator";
|
||||
import { isValidDocument } from "./validators/document-validator";
|
||||
import sanitizeHtml from "sanitize-html";
|
||||
|
||||
/**
|
||||
* Schemas de validação usando Zod
|
||||
* Garante type-safety e validação robusta de dados de entrada
|
||||
*/
|
||||
|
||||
// Schema para email - validação RFC 5322
|
||||
export const emailSchema = z
|
||||
.string()
|
||||
.min(1, "Email é obrigatório")
|
||||
.email("Formato de email inválido")
|
||||
.refine((email) => validator.isEmail(email), {
|
||||
message: "Email inválido",
|
||||
})
|
||||
.transform((email) => email.trim().toLowerCase());
|
||||
|
||||
// Schema para telefone brasileiro
|
||||
export const phoneSchema = z
|
||||
.string()
|
||||
.min(1, "Telefone é obrigatório")
|
||||
.refine((phone) => {
|
||||
// Remove todos os caracteres não numéricos
|
||||
const cleaned = phone.replace(/[^\d]/g, "");
|
||||
// Deve ter 10 ou 11 dígitos (telefone fixo ou celular com DDD)
|
||||
return cleaned.length >= 10 && cleaned.length <= 11;
|
||||
}, "Telefone deve ter 10 ou 11 dígitos (incluindo DDD)")
|
||||
.transform((phone) => phone.replace(/[^\d]/g, ""));
|
||||
|
||||
// Schema para CPF/CNPJ
|
||||
export const documentSchema = z
|
||||
.string()
|
||||
.min(1, "CPF/CNPJ é obrigatório")
|
||||
.refine((doc) => isValidDocument(doc), {
|
||||
message: "CPF ou CNPJ inválido",
|
||||
})
|
||||
.transform((doc) => doc.replace(/[^\d]/g, "")); // Normaliza removendo formatação
|
||||
|
||||
// Schema para nome (alfabético, sem números)
|
||||
export const nameSchema = z
|
||||
.string()
|
||||
.min(3, "Nome deve ter no mínimo 3 caracteres")
|
||||
.max(100, "Nome deve ter no máximo 100 caracteres")
|
||||
.refine(
|
||||
(name) => /^[A-Za-zÀ-ÿ\s'-]+$/.test(name.trim()),
|
||||
"Nome deve conter apenas letras, espaços e acentos"
|
||||
)
|
||||
.transform((name) => name.trim());
|
||||
|
||||
// Schema para mensagem (sanitiza HTML e previne XSS)
|
||||
export const messageSchema = z
|
||||
.string()
|
||||
.max(280, "Mensagem deve ter no máximo 280 caracteres")
|
||||
.transform((message) => {
|
||||
// Sanitiza HTML para prevenir XSS
|
||||
const sanitized = sanitizeHtml(message, {
|
||||
allowedTags: [], // Remove todas as tags HTML
|
||||
allowedAttributes: {},
|
||||
});
|
||||
return sanitized.trim();
|
||||
})
|
||||
.optional();
|
||||
|
||||
// Schema para tier de doação
|
||||
export const donationTierSchema = z.enum(
|
||||
["coffee", "snack", "lunch", "custom"],
|
||||
{
|
||||
message: "Tier de doação inválido",
|
||||
}
|
||||
);
|
||||
|
||||
// Schema para valor customizado
|
||||
export const customAmountSchema = z
|
||||
.number()
|
||||
.min(5, "Valor mínimo é R$ 5,00")
|
||||
.max(500, "Valor máximo é R$ 500,00");
|
||||
|
||||
// Schema completo para criação de doação
|
||||
export const createDonationSchema = z
|
||||
.object({
|
||||
tier: donationTierSchema,
|
||||
anonymous: z.boolean().optional().default(false),
|
||||
customAmount: z.number().optional(),
|
||||
})
|
||||
.and(
|
||||
z.union([
|
||||
// Doação anônima - não precisa de dados pessoais
|
||||
z.object({
|
||||
anonymous: z.literal(true),
|
||||
}),
|
||||
// Doação identificada - precisa de todos os dados
|
||||
z.object({
|
||||
anonymous: z.literal(false),
|
||||
donorName: nameSchema,
|
||||
donorEmail: emailSchema,
|
||||
donorPhone: phoneSchema,
|
||||
donorTaxId: documentSchema,
|
||||
donorMessage: messageSchema,
|
||||
}),
|
||||
])
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
// Se tier for custom, customAmount é obrigatório
|
||||
if (data.tier === "custom") {
|
||||
return data.customAmount !== undefined;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "customAmount é obrigatório quando tier é 'custom'",
|
||||
path: ["customAmount"],
|
||||
}
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
// Se tier for custom, customAmount deve estar no range válido
|
||||
if (data.tier === "custom" && data.customAmount) {
|
||||
return data.customAmount >= 5 && data.customAmount <= 500;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "Valor deve estar entre R$ 5,00 e R$ 500,00",
|
||||
path: ["customAmount"],
|
||||
}
|
||||
);
|
||||
|
||||
// Schema para verificação de status
|
||||
export const verifyStatusSchema = z.object({
|
||||
pixId: z.string().min(1, "pixId é obrigatório"),
|
||||
});
|
||||
|
||||
// Schema para verificação de billing
|
||||
export const verifyBillingSchema = z.object({
|
||||
billingId: z.string().min(1, "ID da cobrança é obrigatório"),
|
||||
});
|
||||
|
||||
// Type exports para uso no código
|
||||
export type CreateDonationInput = z.infer<typeof createDonationSchema>;
|
||||
export type VerifyStatusInput = z.infer<typeof verifyStatusSchema>;
|
||||
export type VerifyBillingInput = z.infer<typeof verifyBillingSchema>;
|
||||
Reference in new issue
Block a user