Files
pagodera/lib/rate-limiter.ts
T
Evandro Fonseca Junior 55eebde0ba feat: add user authentication UI to header component
Add login and register dialogs, user dropdown menu with profile link and logout option, and integrate with session management hook. Prevent hydration mismatches by conditionally rendering auth elements after mount. Update package-lock.json with new @esbuild dependencies for build tooling.
2025-11-04 02:33:29 -03:00

186 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(),
},
}
);
}