123 lines
3.7 KiB
TypeScript
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;
|