189 lines
4.3 KiB
TypeScript
189 lines
4.3 KiB
TypeScript
/**
|
|
* Rate Limiter para ambiente serverless
|
|
* Utiliza Map em memória com TTL (Time To Live)
|
|
* Adequado para serverless de curta duração
|
|
*/
|
|
|
|
interface RateLimitConfig {
|
|
maxRequests: number;
|
|
windowMs: number;
|
|
}
|
|
|
|
interface RateLimitEntry {
|
|
count: number;
|
|
resetAt: number;
|
|
}
|
|
|
|
// Cache em memória de requisições por IP
|
|
const requestCache = new Map<string, RateLimitEntry>();
|
|
|
|
// Configuração de rate limits por endpoint
|
|
const RATE_LIMITS: Record<string, RateLimitConfig> = {
|
|
"/api/donations/create": {
|
|
maxRequests: 5,
|
|
windowMs: 10 * 60 * 1000, // 10 minutos
|
|
},
|
|
"/api/donations/status": {
|
|
maxRequests: 30,
|
|
windowMs: 60 * 1000, // 1 minuto
|
|
},
|
|
"/api/donations/verify": {
|
|
maxRequests: 20,
|
|
windowMs: 60 * 1000, // 1 minuto
|
|
},
|
|
"/api/webhooks/abacatepay": {
|
|
maxRequests: 100,
|
|
windowMs: 60 * 1000, // 1 minuto
|
|
},
|
|
"/api/auth/register": {
|
|
maxRequests: 5,
|
|
windowMs: 10 * 60 * 1000, // 10 minutos
|
|
},
|
|
"/api/auth/login": {
|
|
maxRequests: 10,
|
|
windowMs: 60 * 1000, // 1 minuto
|
|
},
|
|
"/api/auth/refresh": {
|
|
maxRequests: 20,
|
|
windowMs: 60 * 1000, // 1 minuto
|
|
},
|
|
"/api/auth/me": {
|
|
maxRequests: 30,
|
|
windowMs: 60 * 1000, // 1 minuto
|
|
},
|
|
};
|
|
|
|
// Limpar cache expirado a cada 5 minutos
|
|
setInterval(
|
|
() => {
|
|
const now = Date.now();
|
|
let cleaned = 0;
|
|
|
|
for (const [key, entry] of requestCache.entries()) {
|
|
if (now > entry.resetAt) {
|
|
requestCache.delete(key);
|
|
cleaned++;
|
|
}
|
|
}
|
|
|
|
if (cleaned > 0) {
|
|
console.log(`🧹 Rate limiter: ${cleaned} entradas expiradas removidas`);
|
|
}
|
|
},
|
|
5 * 60 * 1000,
|
|
);
|
|
|
|
/**
|
|
* Verifica se uma requisição excede o rate limit
|
|
* @param ip - Endereço IP do cliente
|
|
* @param path - Caminho da requisição (ex: /api/donations/create)
|
|
* @returns Objeto com informações sobre o rate limit
|
|
*/
|
|
export function checkRateLimit(
|
|
ip: string,
|
|
path: string,
|
|
): {
|
|
allowed: boolean;
|
|
remaining: number;
|
|
resetAt: number;
|
|
limit: number;
|
|
} {
|
|
const config = RATE_LIMITS[path];
|
|
|
|
// Se não há config para este path, permite tudo
|
|
if (!config) {
|
|
return {
|
|
allowed: true,
|
|
remaining: Infinity,
|
|
resetAt: Date.now() + 60000,
|
|
limit: Infinity,
|
|
};
|
|
}
|
|
|
|
const key = `${ip}:${path}`;
|
|
const now = Date.now();
|
|
const entry = requestCache.get(key);
|
|
|
|
// Se não existe entrada ou expirou, criar nova
|
|
if (!entry || now > entry.resetAt) {
|
|
const newEntry: RateLimitEntry = {
|
|
count: 1,
|
|
resetAt: now + config.windowMs,
|
|
};
|
|
requestCache.set(key, newEntry);
|
|
|
|
return {
|
|
allowed: true,
|
|
remaining: config.maxRequests - 1,
|
|
resetAt: newEntry.resetAt,
|
|
limit: config.maxRequests,
|
|
};
|
|
}
|
|
|
|
// Se existe e não expirou, incrementar contador
|
|
entry.count++;
|
|
|
|
if (entry.count > config.maxRequests) {
|
|
return {
|
|
allowed: false,
|
|
remaining: 0,
|
|
resetAt: entry.resetAt,
|
|
limit: config.maxRequests,
|
|
};
|
|
}
|
|
|
|
return {
|
|
allowed: true,
|
|
remaining: config.maxRequests - entry.count,
|
|
resetAt: entry.resetAt,
|
|
limit: config.maxRequests,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Extrai o IP real de uma requisição
|
|
* Considera headers de proxies/CDN (x-forwarded-for, x-real-ip)
|
|
*/
|
|
export function getClientIP(request: Request): string {
|
|
// Tentar x-forwarded-for primeiro (pode ter múltiplos IPs)
|
|
const forwardedFor = request.headers.get("x-forwarded-for");
|
|
if (forwardedFor) {
|
|
// Pegar apenas o primeiro IP (cliente original)
|
|
return forwardedFor.split(",")[0].trim();
|
|
}
|
|
|
|
// Tentar x-real-ip
|
|
const realIP = request.headers.get("x-real-ip");
|
|
if (realIP) {
|
|
return realIP.trim();
|
|
}
|
|
|
|
// Fallback: usar conexão remota
|
|
return request.headers.get("cf-connecting-ip") || "unknown";
|
|
}
|
|
|
|
/**
|
|
* Middleware helper para criar resposta de rate limit
|
|
*/
|
|
export function createRateLimitResponse(resetAt: number): Response {
|
|
const retryAfter = Math.ceil((resetAt - Date.now()) / 1000);
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
error: "Rate limit excedido",
|
|
message: "Muitas requisições. Tente novamente mais tarde.",
|
|
retryAfter,
|
|
}),
|
|
{
|
|
status: 429,
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"Retry-After": retryAfter.toString(),
|
|
"X-RateLimit-Limit": "1",
|
|
"X-RateLimit-Remaining": "0",
|
|
"X-RateLimit-Reset": new Date(resetAt).toISOString(),
|
|
},
|
|
},
|
|
);
|
|
}
|