Files
sgse-app/packages/backend/convex/http.ts

123 lines
3.7 KiB
TypeScript

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<string, string> = { '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<string, string> = {};
request.headers.forEach((value, key) => {
headers[key] = value;
});
// Extrair query params
const queryParams: Record<string, string> = {};
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;