import { httpRouter } from 'convex/server'; import { authComponent, createAuth } from './auth'; import { httpAction } from './_generated/server'; import { api } from './_generated/api'; import { getClientIP } from './utils/getClientIP'; const http = httpRouter(); // Action HTTP para análise de segurança de requisições // Pode ser chamada do frontend ou de outros sistemas http.route({ path: '/security/analyze', method: 'POST', handler: httpAction(async (ctx, request) => { const url = new URL(request.url); const method = request.method; // Extrair IP do cliente const ipOrigem = getClientIP(request) ?? '0.0.0.0'; // Enforcement (blacklist + rate limit) antes de processar const enforcement = await ctx.runMutation(api.security.enforceRequest, { ip: ipOrigem, path: url.pathname, method }); if (!enforcement.allowed) { const headers: Record = { 'Content-Type': 'application/json' }; if (enforcement.retryAfterMs) { headers['Retry-After'] = String(Math.ceil(enforcement.retryAfterMs / 1000)); } return new Response(JSON.stringify(enforcement), { status: enforcement.status, headers }); } // Extrair headers const headers: Record = {}; request.headers.forEach((value, key) => { headers[key] = value; }); // Extrair query params const queryParams: Record = {}; url.searchParams.forEach((value, key) => { queryParams[key] = value; }); // Extrair body se disponível let body: string | undefined; try { body = await request.text(); } catch { // Ignorar erros ao ler body } // Analisar requisição para detectar ataques ANTES de processar const analise = await ctx.runMutation(api.security.analisarRequisicaoHTTP, { url: url.pathname + url.search, method, headers, body, queryParams, ipOrigem, userAgent: request.headers.get('user-agent') ?? undefined }); // Se ataque detectado e bloqueio automático aplicado, retornar 403 imediatamente if (analise.ataqueDetectado && analise.bloqueadoAutomatico) { return new Response( JSON.stringify({ ataqueDetectado: true, bloqueado: true, tipoAtaque: analise.tipoAtaque, severidade: analise.severidade, mensagem: 'Ataque detectado e bloqueado automaticamente' }), { status: 403, headers: { 'Content-Type': 'application/json' } } ); } // Se ataque detectado mas sem bloqueio automático, retornar resultado normalmente return new Response(JSON.stringify(analise), { status: 200, headers: { 'Content-Type': 'application/json' } }); }) }); // Seed de rate limit para ambiente de desenvolvimento http.route({ path: '/security/rate-limit/seed-dev', method: 'POST', handler: httpAction(async (ctx) => { // Hardening: endpoint apenas para dev/staging. // Em Convex, NODE_ENV pode ser 'production' mesmo em dev local, // então também aceitamos deployments locais "anonymous-*", ou uma flag explícita. const deployment = process.env.CONVEX_DEPLOYMENT; const siteUrl = process.env.SITE_URL || process.env.CONVEX_SITE_URL; const enabled = process.env.SECURITY_DEV_TOOLS === 'true' || (typeof siteUrl === 'string' && /localhost|127\.0\.0\.1/i.test(siteUrl)) || (typeof deployment === 'string' && deployment.startsWith('anonymous-')) || process.env.NODE_ENV !== 'production'; if (!enabled) { return new Response('Not found', { status: 404 }); } const resultado = await ctx.runMutation(api.security.seedRateLimitDev, {}); return new Response(JSON.stringify(resultado), { status: 200, headers: { 'Content-Type': 'application/json' } }); }) }); authComponent.registerRoutes(http, createAuth); export default http;