feat: implement security enhancements for Jitsi integration, including JWT token generation and automatic blocking of detected attacks, improving system resilience and user authentication

This commit is contained in:
2026-01-12 04:34:00 -03:00
parent b965514e53
commit 664d90c2e0
27 changed files with 6174 additions and 329 deletions

View File

@@ -3,6 +3,7 @@ import { v } from 'convex/values';
import { internalMutation, mutation, MutationCtx, query, QueryCtx } from './_generated/server';
import { internal, api, components } from './_generated/api';
import type { Id } from './_generated/dataModel';
import { getCurrentUserFunction } from './auth';
import type {
AtaqueCiberneticoTipo,
SeveridadeSeguranca,
@@ -38,6 +39,35 @@ type RegistroGeo = {
longitude?: number;
};
async function assertAdmin(ctx: QueryCtx | MutationCtx) {
const usuarioAtual = await getCurrentUserFunction(ctx);
if (!usuarioAtual) {
throw new Error('acesso_negado');
}
if (!usuarioAtual.roleId) {
throw new Error('acesso_negado');
}
const role = await ctx.db.get(usuarioAtual.roleId);
if (!role || role.admin !== true) {
throw new Error('acesso_negado');
}
return usuarioAtual;
}
function assertDevOnly() {
// Em produção, endpoints/mutations de teste devem ser desabilitados.
// Observação: em ambientes Convex, NODE_ENV pode vir como '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) throw new Error('acesso_negado');
}
type RegistroEventoArgs = {
descricao?: string;
tipoAtaque?: AtaqueCiberneticoTipo;
@@ -83,7 +113,6 @@ const ATAQUES_PRIORITARIOS: Array<AtaqueCiberneticoTipo> = [
'brute_force',
'supply_chain',
'malware',
'engenharia_social',
'cve_exploit',
'bec',
'side_channel'
@@ -103,7 +132,6 @@ const BASE_SEVERIDADE: Record<AtaqueCiberneticoTipo, SeveridadeSeguranca> = {
xxe: 'alto',
man_in_the_middle: 'alto',
ddos: 'alto',
engenharia_social: 'moderado',
cve_exploit: 'alto',
apt: 'critico',
zero_day: 'critico',
@@ -298,7 +326,6 @@ const KEYWORDS: Record<AtaqueCiberneticoTipo, RegExp[]> = {
],
man_in_the_middle: [/mitm/i, /man-in-the-middle/i, /ssl strip/i, /tls downgrade/i],
ddos: [/ddos/i, /flood/i, /pps/i, /distributed denial/i, /traffic flood/i],
engenharia_social: [/social/i, /engenharia/i, /social engineering/i],
cve_exploit: [/cve-\d{4}-\d+/i, /exploit/i, /cve/i],
apt: [/apt/i, /persistent/i, /advanced persistent threat/i],
zero_day: [/zero[-\s]?day/i, /unknown exploit/i, /0-day/i],
@@ -332,7 +359,6 @@ const ataqueValidator = v.union(
v.literal('xxe'),
v.literal('man_in_the_middle'),
v.literal('ddos'),
v.literal('engenharia_social'),
v.literal('cve_exploit'),
v.literal('apt'),
v.literal('zero_day'),
@@ -404,6 +430,79 @@ const acaoIncidenteValidator = v.union(
const acaoOrigemValidator = v.union(v.literal('automatico'), v.literal('manual'));
export const enforceRequest = mutation({
args: {
ip: v.string(),
path: v.string(),
method: v.string()
},
returns: v.object({
allowed: v.boolean(),
status: v.number(),
reason: v.optional(v.string()),
retryAfterMs: v.optional(v.number())
}),
handler: async (ctx, args) => {
const agora = Date.now();
const ip = args.ip.trim();
const path = args.path.trim() || '/';
const pathKey = path.replace(/^\/+/, '');
// 1) Blacklist enforcement (somente IP)
const registroIp = await ctx.db
.query('ipReputation')
.withIndex('by_indicador', (q) => q.eq('indicador', ip))
.order('desc')
.first();
if (registroIp && registroIp.categoria === 'ip' && registroIp.blacklist === true) {
const ativo = !registroIp.bloqueadoAte || registroIp.bloqueadoAte > agora;
if (ativo) {
return {
allowed: false,
status: 403,
reason: 'ip_blacklisted'
};
}
}
// 2) Rate limit enforcement (endpoint + IP)
// - endpoint rules usam o "pathKey" (ex.: api/auth/sign-in/email)
// - ip rules usam o IP e namespace por endpoint
const endpointCheck = await aplicarRateLimit(ctx, 'endpoint', pathKey || 'root', pathKey || 'root');
if (!endpointCheck.permitido) {
return {
allowed: false,
status: 429,
reason: endpointCheck.motivo ?? 'rate_limited',
retryAfterMs: endpointCheck.retryAfter
};
}
const ipCheck = await aplicarRateLimit(ctx, 'ip', ip, pathKey || 'root');
if (!ipCheck.permitido) {
return {
allowed: false,
status: 429,
reason: ipCheck.motivo ?? 'rate_limited',
retryAfterMs: ipCheck.retryAfter
};
}
// NOTE: para estratégias "throttle", o helper pode retornar retryAfter mas permitir.
// Aqui optamos por permitir, deixando o servidor decidir se atrasa ou apenas sinaliza.
const retryAfterMs =
(endpointCheck.retryAfter ?? 0) > 0 ? endpointCheck.retryAfter : ipCheck.retryAfter;
return {
allowed: true,
status: 200,
reason: undefined,
retryAfterMs: retryAfterMs && retryAfterMs > 0 ? retryAfterMs : undefined
};
}
});
// Função para analisar string e detectar ataques
function analisarStringParaAtaques(texto: string): AtaqueCiberneticoTipo | null {
if (!texto) return null;
@@ -700,6 +799,7 @@ export const listarEventosSeguranca = query({
})
),
handler: async (ctx, args) => {
await assertAdmin(ctx);
const limit = args.limit && args.limit > 0 ? Math.min(args.limit, 500) : 100;
const janelaInicial = args.apos ?? Date.now() - 6 * 60 * 60 * 1000;
@@ -809,6 +909,7 @@ export const obterVisaoCamadas = query({
})
}),
handler: async (ctx, args) => {
await assertAdmin(ctx);
const agora = Date.now();
const periodoMs = Math.max(1, args.periodoHoras ?? 6) * 60 * 60 * 1000;
const inicioJanela = agora - periodoMs;
@@ -899,6 +1000,7 @@ export const listarReputacoes = query({
})
),
handler: async (ctx, args) => {
await assertAdmin(ctx);
const limit = args.limit && args.limit > 0 ? Math.min(args.limit, 500) : 200;
const builder =
@@ -933,7 +1035,6 @@ export const listarReputacoes = query({
export const atualizarReputacaoIndicador = mutation({
args: {
usuarioId: v.id('usuarios'),
indicador: v.string(),
categoria: indicadorCategoriaValidator,
acao: v.union(
@@ -953,11 +1054,12 @@ export const atualizarReputacaoIndicador = mutation({
status: v.string()
}),
handler: async (ctx, args) => {
const usuarioAtual = await assertAdmin(ctx);
// Aplicar rate limiting por usuário
const rateLimitResult = await aplicarRateLimit(
ctx,
'usuario',
args.usuarioId,
usuarioAtual._id,
'atualizarReputacaoIndicador'
);
if (!rateLimitResult.permitido) {
@@ -995,7 +1097,7 @@ export const atualizarReputacaoIndicador = mutation({
eventoId: await ctx.db.insert('securityEvents', {
referencia: `auto-${args.indicador}-${agora}`,
timestamp: agora,
tipoAtaque: 'engenharia_social',
tipoAtaque: 'malware',
severidade: 'informativo',
status: 'contido',
descricao: 'Registro criado via painel de reputação.',
@@ -1016,13 +1118,13 @@ export const atualizarReputacaoIndicador = mutation({
tags: ['reputacao'],
referenciasExternas: undefined,
correlacoes: undefined,
criadoPor: args.usuarioId,
criadoPor: usuarioAtual._id,
atualizadoEm: agora
}),
tipo: args.acao === 'forcar_blacklist' ? 'block_ip' : 'custom',
origem: 'manual',
status: 'concluido',
executadoPor: args.usuarioId,
executadoPor: usuarioAtual._id,
detalhes: args.comentario,
resultado: 'Registro inicial',
relacionadoA: undefined,
@@ -1071,7 +1173,6 @@ export const atualizarReputacaoIndicador = mutation({
export const configurarRegraPorta = mutation({
args: {
usuarioId: v.id('usuarios'),
regraId: v.optional(v.id('portRules')),
porta: v.number(),
protocolo: protocoloValidator,
@@ -1088,6 +1189,7 @@ export const configurarRegraPorta = mutation({
status: v.string()
}),
handler: async (ctx, args) => {
const usuarioAtual = await assertAdmin(ctx);
const agora = Date.now();
const expiraEm =
args.temporario && args.duracaoSegundos ? agora + args.duracaoSegundos * 1000 : undefined;
@@ -1100,7 +1202,7 @@ export const configurarRegraPorta = mutation({
temporario: args.temporario,
duracaoSegundos: args.duracaoSegundos,
expiraEm,
atualizadoPor: args.usuarioId,
atualizadoPor: usuarioAtual._id,
atualizadoEm: agora,
severidadeMin: args.severidadeMin,
notas: args.notas,
@@ -1118,8 +1220,8 @@ export const configurarRegraPorta = mutation({
severidadeMin: args.severidadeMin,
duracaoSegundos: args.duracaoSegundos,
expiraEm,
criadoPor: args.usuarioId,
atualizadoPor: args.usuarioId,
criadoPor: usuarioAtual._id,
atualizadoPor: usuarioAtual._id,
criadoEm: agora,
atualizadoEm: agora,
notas: args.notas,
@@ -1150,6 +1252,7 @@ export const listarRegrasPorta = query({
})
),
handler: async (ctx, args) => {
await assertAdmin(ctx);
const builder =
args.acao === undefined
? ctx.db.query('portRules')
@@ -1203,13 +1306,17 @@ export const registrarAcaoIncidente = mutation({
acaoId: v.id('incidentActions')
}),
handler: async (ctx, args) => {
const usuarioAtual = await assertAdmin(ctx);
if (args.executadoPor && args.executadoPor !== usuarioAtual._id) {
throw new Error('acesso_negado');
}
const agora = Date.now();
const acaoId = await ctx.db.insert('incidentActions', {
eventoId: args.eventoId,
tipo: args.tipo,
origem: args.origem,
status: args.status ?? 'concluido',
executadoPor: args.executadoPor,
executadoPor: usuarioAtual._id,
detalhes: args.detalhes,
resultado: args.resultado,
relacionadoA: args.relacionadoA,
@@ -1223,7 +1330,6 @@ export const registrarAcaoIncidente = mutation({
export const solicitarRelatorioSeguranca = mutation({
args: {
solicitanteId: v.id('usuarios'),
filtros: v.object({
dataInicio: v.number(),
dataFim: v.number(),
@@ -1238,8 +1344,9 @@ export const solicitarRelatorioSeguranca = mutation({
relatorioId: v.id('reportRequests')
}),
handler: async (ctx, args) => {
const usuarioAtual = await assertAdmin(ctx);
const relatorioId = await ctx.db.insert('reportRequests', {
solicitanteId: args.solicitanteId,
solicitanteId: usuarioAtual._id,
filtros: {
dataInicio: args.filtros.dataInicio,
dataFim: args.filtros.dataFim,
@@ -1286,6 +1393,7 @@ export const listarRelatoriosRecentes = query({
})
),
handler: async (ctx, args) => {
await assertAdmin(ctx);
const max = Math.min(args.limit ?? 10, 50);
const rows = await ctx.db
.query('reportRequests')
@@ -1311,6 +1419,7 @@ export const healthStatus = query({
pendingReports: v.number()
}),
handler: async (ctx) => {
await assertAdmin(ctx);
// Contar rapidamente quantos relatórios pendentes existem (limitado)
const pending = await ctx.db
.query('reportRequests')
@@ -1332,6 +1441,7 @@ export const deletarRelatorio = mutation({
},
returns: v.object({ success: v.boolean() }),
handler: async (ctx, args) => {
await assertAdmin(ctx);
const doc = await ctx.db.get(args.relatorioId);
if (!doc) {
return { success: false };
@@ -1440,6 +1550,7 @@ export const listarAlertConfigs = query({
})
),
handler: async (ctx, args) => {
await assertAdmin(ctx);
const max = Math.min(args.limit ?? 100, 200);
const rows = await ctx.db
.query('alertConfigs')
@@ -1472,11 +1583,11 @@ export const salvarAlertConfig = mutation({
severidadeMin: severidadeValidator,
tiposAtaque: v.optional(v.array(ataqueValidator)),
reenvioMin: v.number(),
templateCodigo: v.optional(v.string()), // Template a ser usado
criadoPor: v.id('usuarios')
templateCodigo: v.optional(v.string()) // Template a ser usado
},
returns: v.object({ _id: v.id('alertConfigs') }),
handler: async (ctx, args) => {
const usuarioAtual = await assertAdmin(ctx);
const agora = Date.now();
if (args.configId) {
await ctx.db.patch(args.configId, {
@@ -1501,7 +1612,7 @@ export const salvarAlertConfig = mutation({
tiposAtaque: args.tiposAtaque,
reenvioMin: args.reenvioMin,
templateCodigo: args.templateCodigo ?? 'incidente_critico', // Padrão
criadoPor: args.criadoPor,
criadoPor: usuarioAtual._id,
criadoEm: agora,
atualizadoEm: agora
});
@@ -1513,6 +1624,7 @@ export const deletarAlertConfig = mutation({
args: { configId: v.id('alertConfigs') },
returns: v.null(),
handler: async (ctx, args) => {
await assertAdmin(ctx);
await ctx.db.delete(args.configId);
return null;
}
@@ -1566,10 +1678,9 @@ export const dispararAlertasInternos = internalMutation({
command_injection: 'Command Injection',
nosql_injection: 'NoSQL Injection',
xxe: 'XXE',
man_in_the_middle: 'MITM',
ddos: 'DDoS',
engenharia_social: 'Engenharia Social',
cve_exploit: 'Exploração de CVE',
man_in_the_middle: 'MITM',
ddos: 'DDoS',
cve_exploit: 'Exploração de CVE',
apt: 'APT',
zero_day: 'Zero-Day',
supply_chain: 'Supply Chain',
@@ -2113,7 +2224,6 @@ async function aplicarRateLimit(
export const criarConfigRateLimit = mutation({
args: {
usuarioId: v.id('usuarios'),
nome: v.string(),
tipo: v.union(
v.literal('ip'),
@@ -2137,6 +2247,7 @@ export const criarConfigRateLimit = mutation({
},
returns: v.id('rateLimitConfig'),
handler: async (ctx, args) => {
const usuarioAtual = await assertAdmin(ctx);
const agora = Date.now();
const configId = await ctx.db.insert('rateLimitConfig', {
nome: args.nome,
@@ -2149,7 +2260,7 @@ export const criarConfigRateLimit = mutation({
bloqueioTemporarioSegundos: args.bloqueioTemporarioSegundos,
ativo: true,
prioridade: args.prioridade ?? 0,
criadoPor: args.usuarioId,
criadoPor: usuarioAtual._id,
atualizadoPor: undefined,
criadoEm: agora,
atualizadoEm: agora,
@@ -2164,7 +2275,6 @@ export const criarConfigRateLimit = mutation({
export const atualizarConfigRateLimit = mutation({
args: {
configId: v.id('rateLimitConfig'),
usuarioId: v.id('usuarios'),
nome: v.optional(v.string()),
limite: v.optional(v.number()),
janelaSegundos: v.optional(v.number()),
@@ -2182,6 +2292,7 @@ export const atualizarConfigRateLimit = mutation({
},
returns: v.null(),
handler: async (ctx, args) => {
const usuarioAtual = await assertAdmin(ctx);
const config = await ctx.db.get(args.configId);
if (!config) {
throw new Error('Configuração de rate limit não encontrada');
@@ -2201,7 +2312,7 @@ export const atualizarConfigRateLimit = mutation({
atualizadoPor: Id<'usuarios'>;
atualizadoEm: number;
} = {
atualizadoPor: args.usuarioId,
atualizadoPor: usuarioAtual._id,
atualizadoEm: Date.now()
};
@@ -2257,6 +2368,7 @@ export const listarConfigsRateLimit = query({
})
),
handler: async (ctx, args) => {
await assertAdmin(ctx);
let builder;
if (args.tipo !== undefined) {
const tipo = args.tipo;
@@ -2311,7 +2423,8 @@ export const analisarRequisicaoHTTP = mutation({
ataqueDetectado: v.boolean(),
tipoAtaque: v.optional(ataqueValidator),
severidade: v.optional(severidadeValidator),
eventoId: v.optional(v.id('securityEvents'))
eventoId: v.optional(v.id('securityEvents')),
bloqueadoAutomatico: v.optional(v.boolean())
}),
handler: async (ctx, args) => {
// Combinar todos os dados da requisição para análise
@@ -2336,13 +2449,38 @@ export const analisarRequisicaoHTTP = mutation({
ataqueDetectado: false,
tipoAtaque: undefined,
severidade: undefined,
eventoId: undefined
eventoId: undefined,
bloqueadoAutomatico: undefined
};
}
// Calcular severidade
const severidade = calcularSeveridade(tipoAtaque, undefined, undefined);
// Verificar configuração de bloqueio automático
const autoBlockConfig = await ctx.db
.query('autoBlockConfig')
.withIndex('by_tipo', (q) => q.eq('tipoAtaque', tipoAtaque))
.filter((q) => q.eq(q.field('ativo'), true))
.first();
let bloqueadoAutomatico = false;
let bloqueadoAte: number | undefined = undefined;
// Aplicar bloqueio automático se configurado
if (
autoBlockConfig &&
autoBlockConfig.bloquearAutomatico &&
SEVERIDADE_SCORE[severidade] >= SEVERIDADE_SCORE[autoBlockConfig.severidadeMinima]
) {
bloqueadoAutomatico = true;
// Calcular data de expiração se duração definida
if (autoBlockConfig.duracaoBloqueioSegundos) {
bloqueadoAte = Date.now() + autoBlockConfig.duracaoBloqueioSegundos * 1000;
}
}
// Permitir que o chamador informe o destino/protocolo via query string em cenários de dev/teste
const destinoIp =
(args.queryParams &&
@@ -2353,13 +2491,20 @@ export const analisarRequisicaoHTTP = mutation({
// Registrar evento de segurança
const referencia = `http_${Date.now()}_${Math.random().toString(36).substring(7)}`;
const agora = Date.now();
const tags = ['detecção_automática', 'http', tipoAtaque];
if (bloqueadoAutomatico) {
tags.push('bloqueio_automatico');
}
const eventoId = await ctx.db.insert('securityEvents', {
referencia,
timestamp: agora,
tipoAtaque,
severidade,
status: statusInicial(severidade),
descricao: `Ataque ${tipoAtaque} detectado na requisição HTTP ${args.method} ${args.url}`,
descricao: `Ataque ${tipoAtaque} detectado na requisição HTTP ${args.method} ${args.url}${
bloqueadoAutomatico ? ' (BLOQUEADO AUTOMATICAMENTE)' : ''
}`,
origemIp: args.ipOrigem,
protocolo,
transporte: 'tcp',
@@ -2370,28 +2515,30 @@ export const analisarRequisicaoHTTP = mutation({
}
: undefined,
destinoIp: destinoIp ?? undefined,
tags: ['detecção_automática', 'http', tipoAtaque],
tags,
atualizadoEm: agora
});
// Ajustar reputação do IP se fornecido
if (args.ipOrigem) {
const delta = SEVERIDADE_SCORE[severidade] * -10; // Penalidade baseada na severidade
await ajustarReputacao(
ctx,
args.ipOrigem,
'ip',
delta,
severidade,
severidade === 'critico' || severidade === 'alto' ? { blacklist: true } : undefined
);
// Se bloqueio automático ativo, aplicar blacklist
const opcoesBlacklist = bloqueadoAutomatico
? { blacklist: true, bloqueadoAte }
: severidade === 'critico' || severidade === 'alto'
? { blacklist: true }
: undefined;
await ajustarReputacao(ctx, args.ipOrigem, 'ip', delta, severidade, opcoesBlacklist);
}
return {
ataqueDetectado: true,
tipoAtaque,
severidade,
eventoId
eventoId,
bloqueadoAutomatico
};
}
});
@@ -2531,6 +2678,7 @@ export const criarEventosTeste = mutation({
eventosIds: v.array(v.id('securityEvents'))
}),
handler: async (ctx, args) => {
assertDevOnly();
const quantidade = args.quantidade ?? 10;
const eventosIds: Id<'securityEvents'>[] = [];
const agora = Date.now();
@@ -2701,11 +2849,11 @@ export const monitorarLogsLogin = internalMutation({
export const deletarConfigRateLimit = mutation({
args: {
configId: v.id('rateLimitConfig'),
usuarioId: v.id('usuarios')
configId: v.id('rateLimitConfig')
},
returns: v.null(),
handler: async (ctx, args) => {
await assertAdmin(ctx);
const config = await ctx.db.get(args.configId);
if (!config) {
throw new Error('Configuração de rate limit não encontrada');
@@ -2723,8 +2871,9 @@ export const seedRateLimitDev = mutation({
criadosOuAtualizados: v.number()
}),
handler: async (ctx) => {
assertDevOnly();
let count = 0;
// Obter um usuário existente para campos de auditoria
// Em dev, usar qualquer usuário existente para auditoria
const algumUsuario = await ctx.db.query('usuarios').order('asc').take(1);
if (algumUsuario.length === 0) {
throw new Error('Seed de rate limit: nenhum usuário encontrado para auditoria (criadoPor).');
@@ -2836,6 +2985,7 @@ export const limparEventosTeste = mutation({
args: {},
returns: v.object({ removidos: v.number() }),
handler: async (ctx) => {
assertDevOnly();
const docs = await ctx.db
.query('securityEvents')
.withIndex('by_timestamp', (q) => q.gte('timestamp', 0))
@@ -2855,11 +3005,11 @@ export const limparEventosTeste = mutation({
// Deletar regra de porta
export const deletarRegraPorta = mutation({
args: {
regraId: v.id('portRules'),
usuarioId: v.id('usuarios')
regraId: v.id('portRules')
},
returns: v.null(),
handler: async (ctx, args) => {
await assertAdmin(ctx);
const regra = await ctx.db.get(args.regraId);
if (!regra) {
throw new Error('Regra de porta não encontrada');
@@ -2868,3 +3018,220 @@ export const deletarRegraPorta = mutation({
return null;
}
});
// ============================================
// CONFIGURAÇÃO DE BLOQUEIO AUTOMÁTICO
// ============================================
// Criar configuração de bloqueio automático
export const criarAutoBlockConfig = mutation({
args: {
tipoAtaque: ataqueValidator,
bloquearAutomatico: v.boolean(),
severidadeMinima: severidadeValidator,
duracaoBloqueioSegundos: v.optional(v.number()),
ativo: v.boolean()
},
returns: v.id('autoBlockConfig'),
handler: async (ctx, args) => {
const usuarioAtual = await assertAdmin(ctx);
const agora = Date.now();
// Verificar se já existe configuração para este tipo de ataque
const existente = await ctx.db
.query('autoBlockConfig')
.withIndex('by_tipo', (q) => q.eq('tipoAtaque', args.tipoAtaque))
.first();
if (existente) {
throw new Error(`Já existe uma configuração para o tipo de ataque ${args.tipoAtaque}`);
}
// Validação: não permitir bloqueio permanente para severidade baixa
if (
args.bloquearAutomatico &&
!args.duracaoBloqueioSegundos &&
(args.severidadeMinima === 'informativo' || args.severidadeMinima === 'baixo')
) {
throw new Error(
'Bloqueio permanente só é permitido para severidade moderada, alta ou crítica'
);
}
const configId = await ctx.db.insert('autoBlockConfig', {
tipoAtaque: args.tipoAtaque,
bloquearAutomatico: args.bloquearAutomatico,
severidadeMinima: args.severidadeMinima,
duracaoBloqueioSegundos: args.duracaoBloqueioSegundos,
ativo: args.ativo,
criadoPor: usuarioAtual._id,
atualizadoPor: undefined,
criadoEm: agora,
atualizadoEm: agora
});
return configId;
}
});
// Atualizar configuração de bloqueio automático
export const atualizarAutoBlockConfig = mutation({
args: {
configId: v.id('autoBlockConfig'),
bloquearAutomatico: v.optional(v.boolean()),
severidadeMinima: v.optional(severidadeValidator),
duracaoBloqueioSegundos: v.optional(v.number()),
ativo: v.optional(v.boolean())
},
returns: v.id('autoBlockConfig'),
handler: async (ctx, args) => {
const usuarioAtual = await assertAdmin(ctx);
const config = await ctx.db.get(args.configId);
if (!config) {
throw new Error('Configuração não encontrada');
}
// Validação: não permitir bloqueio permanente para severidade baixa
const severidadeMinima = args.severidadeMinima ?? config.severidadeMinima;
const bloquearAutomatico = args.bloquearAutomatico ?? config.bloquearAutomatico;
const duracaoBloqueioSegundos =
args.duracaoBloqueioSegundos !== undefined
? args.duracaoBloqueioSegundos
: config.duracaoBloqueioSegundos;
if (
bloquearAutomatico &&
!duracaoBloqueioSegundos &&
(severidadeMinima === 'informativo' || severidadeMinima === 'baixo')
) {
throw new Error(
'Bloqueio permanente só é permitido para severidade moderada, alta ou crítica'
);
}
await ctx.db.patch(args.configId, {
bloquearAutomatico: args.bloquearAutomatico ?? config.bloquearAutomatico,
severidadeMinima: severidadeMinima,
duracaoBloqueioSegundos: duracaoBloqueioSegundos,
ativo: args.ativo ?? config.ativo,
atualizadoPor: usuarioAtual._id,
atualizadoEm: Date.now()
});
return args.configId;
}
});
// Deletar configuração de bloqueio automático
export const deletarAutoBlockConfig = mutation({
args: {
configId: v.id('autoBlockConfig')
},
returns: v.null(),
handler: async (ctx, args) => {
await assertAdmin(ctx);
const config = await ctx.db.get(args.configId);
if (!config) {
throw new Error('Configuração não encontrada');
}
await ctx.db.delete(args.configId);
return null;
}
});
// Listar todas as configurações de bloqueio automático
export const listarAutoBlockConfigs = query({
args: {
ativo: v.optional(v.boolean()),
limit: v.optional(v.number())
},
returns: v.array(
v.object({
_id: v.id('autoBlockConfig'),
tipoAtaque: ataqueValidator,
bloquearAutomatico: v.boolean(),
severidadeMinima: severidadeValidator,
duracaoBloqueioSegundos: v.optional(v.number()),
ativo: v.boolean(),
criadoPor: v.id('usuarios'),
atualizadoPor: v.optional(v.id('usuarios')),
criadoEm: v.number(),
atualizadoEm: v.number()
})
),
handler: async (ctx, args) => {
await assertAdmin(ctx);
const queryInitializer = ctx.db.query('autoBlockConfig');
const query = args.ativo !== undefined
? queryInitializer.withIndex('by_ativo', (q) => {
const ativoValue: boolean = args.ativo!;
return q.eq('ativo', ativoValue);
})
: queryInitializer;
const configs = await query.order('desc').take(args.limit ?? 100);
return configs.map((config) => ({
_id: config._id,
tipoAtaque: config.tipoAtaque,
bloquearAutomatico: config.bloquearAutomatico,
severidadeMinima: config.severidadeMinima,
duracaoBloqueioSegundos: config.duracaoBloqueioSegundos,
ativo: config.ativo,
criadoPor: config.criadoPor,
atualizadoPor: config.atualizadoPor,
criadoEm: config.criadoEm,
atualizadoEm: config.atualizadoEm
}));
}
});
// Obter configuração de bloqueio automático por tipo de ataque
export const obterAutoBlockConfig = query({
args: {
tipoAtaque: ataqueValidator
},
returns: v.union(
v.object({
_id: v.id('autoBlockConfig'),
tipoAtaque: ataqueValidator,
bloquearAutomatico: v.boolean(),
severidadeMinima: severidadeValidator,
duracaoBloqueioSegundos: v.optional(v.number()),
ativo: v.boolean(),
criadoPor: v.id('usuarios'),
atualizadoPor: v.optional(v.id('usuarios')),
criadoEm: v.number(),
atualizadoEm: v.number()
}),
v.null()
),
handler: async (ctx, args) => {
await assertAdmin(ctx);
const config = await ctx.db
.query('autoBlockConfig')
.withIndex('by_tipo', (q) => q.eq('tipoAtaque', args.tipoAtaque))
.filter((q) => q.eq(q.field('ativo'), true))
.first();
if (!config) {
return null;
}
return {
_id: config._id,
tipoAtaque: config.tipoAtaque,
bloquearAutomatico: config.bloquearAutomatico,
severidadeMinima: config.severidadeMinima,
duracaoBloqueioSegundos: config.duracaoBloqueioSegundos,
ativo: config.ativo,
criadoPor: config.criadoPor,
atualizadoPor: config.atualizadoPor,
criadoEm: config.criadoEm,
atualizadoEm: config.atualizadoEm
};
}
});