feat: Introduce structured table definitions in convex/tables for various entities and remove the todos example table.
This commit is contained in:
@@ -1,16 +1,12 @@
|
||||
import { v } from 'convex/values';
|
||||
import {
|
||||
internalMutation,
|
||||
mutation,
|
||||
query
|
||||
} from './_generated/server';
|
||||
import { internalMutation, mutation, query } from './_generated/server';
|
||||
import { internal } from './_generated/api';
|
||||
import type { Id } from './_generated/dataModel';
|
||||
import type {
|
||||
AtaqueCiberneticoTipo,
|
||||
SeveridadeSeguranca,
|
||||
StatusEventoSeguranca
|
||||
} from './schema';
|
||||
} from './tables/security';
|
||||
import type { MutationCtx, QueryCtx } from './_generated/server';
|
||||
import { RateLimiter, SECOND } from '@convex-dev/rate-limiter';
|
||||
import { components } from './_generated/api';
|
||||
@@ -413,9 +409,9 @@ const acaoOrigemValidator = v.union(v.literal('automatico'), v.literal('manual')
|
||||
// Função para analisar string e detectar ataques
|
||||
function analisarStringParaAtaques(texto: string): AtaqueCiberneticoTipo | null {
|
||||
if (!texto) return null;
|
||||
|
||||
|
||||
const textoLower = texto.toLowerCase();
|
||||
|
||||
|
||||
// Verificar cada tipo de ataque em ordem de prioridade
|
||||
for (const tipo of ATAQUES_PRIORITARIOS) {
|
||||
const patterns = KEYWORDS[tipo];
|
||||
@@ -423,7 +419,7 @@ function analisarStringParaAtaques(texto: string): AtaqueCiberneticoTipo | null
|
||||
return tipo;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -591,14 +587,23 @@ export const registrarEventoSeguranca = mutation({
|
||||
handler: async (ctx, args) => {
|
||||
// Aplicar rate limiting por IP se fornecido
|
||||
if (args.origemIp) {
|
||||
const rateLimitResult = await aplicarRateLimit(ctx, 'ip', args.origemIp, 'registrarEventoSeguranca');
|
||||
const rateLimitResult = await aplicarRateLimit(
|
||||
ctx,
|
||||
'ip',
|
||||
args.origemIp,
|
||||
'registrarEventoSeguranca'
|
||||
);
|
||||
if (!rateLimitResult.permitido) {
|
||||
throw new Error(rateLimitResult.motivo ?? 'Rate limit excedido');
|
||||
}
|
||||
}
|
||||
|
||||
const tipo = inferirTipoAtaque(args);
|
||||
const severidade = calcularSeveridade(tipo, args.metricas ?? undefined, args.severidade ?? undefined);
|
||||
const severidade = calcularSeveridade(
|
||||
tipo,
|
||||
args.metricas ?? undefined,
|
||||
args.severidade ?? undefined
|
||||
);
|
||||
const status = statusInicial(severidade);
|
||||
|
||||
const duplicado = await ctx.db
|
||||
@@ -727,10 +732,18 @@ export const listarEventosSeguranca = query({
|
||||
const candidatos = await builder.order('desc').take(limit * 3);
|
||||
const filtrados = candidatos
|
||||
.filter((evento) => {
|
||||
if (args.severidades && args.severidades.length > 0 && !args.severidades.includes(evento.severidade)) {
|
||||
if (
|
||||
args.severidades &&
|
||||
args.severidades.length > 0 &&
|
||||
!args.severidades.includes(evento.severidade)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (args.tiposAtaque && args.tiposAtaque.length > 0 && !args.tiposAtaque.includes(evento.tipoAtaque)) {
|
||||
if (
|
||||
args.tiposAtaque &&
|
||||
args.tiposAtaque.length > 0 &&
|
||||
!args.tiposAtaque.includes(evento.tipoAtaque)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (args.status && args.status.length > 0 && !args.status.includes(evento.status)) {
|
||||
@@ -829,10 +842,7 @@ export const obterVisaoCamadas = query({
|
||||
for (const evento of eventos) {
|
||||
const idx = Math.min(
|
||||
bucketCount - 1,
|
||||
Math.max(
|
||||
0,
|
||||
Math.floor((evento.timestamp - inicioJanela) / bucketSize)
|
||||
)
|
||||
Math.max(0, Math.floor((evento.timestamp - inicioJanela) / bucketSize))
|
||||
);
|
||||
const bucket = series[idx];
|
||||
if (evento.severidade === 'critico') criticos += 1;
|
||||
@@ -893,11 +903,12 @@ export const listarReputacoes = query({
|
||||
handler: async (ctx, args) => {
|
||||
const limit = args.limit && args.limit > 0 ? Math.min(args.limit, 500) : 200;
|
||||
|
||||
const builder = args.lista === undefined
|
||||
? ctx.db.query('ipReputation')
|
||||
: args.lista === 'blacklist'
|
||||
? ctx.db.query('ipReputation').withIndex('by_blacklist', (q) => q.eq('blacklist', true))
|
||||
: ctx.db.query('ipReputation').withIndex('by_whitelist', (q) => q.eq('whitelist', true));
|
||||
const builder =
|
||||
args.lista === undefined
|
||||
? ctx.db.query('ipReputation')
|
||||
: args.lista === 'blacklist'
|
||||
? ctx.db.query('ipReputation').withIndex('by_blacklist', (q) => q.eq('blacklist', true))
|
||||
: ctx.db.query('ipReputation').withIndex('by_whitelist', (q) => q.eq('whitelist', true));
|
||||
|
||||
const docs = await builder.order('desc').take(limit * 2);
|
||||
const filtrados = docs
|
||||
@@ -945,7 +956,12 @@ export const atualizarReputacaoIndicador = mutation({
|
||||
}),
|
||||
handler: async (ctx, args) => {
|
||||
// Aplicar rate limiting por usuário
|
||||
const rateLimitResult = await aplicarRateLimit(ctx, 'usuario', args.usuarioId, 'atualizarReputacaoIndicador');
|
||||
const rateLimitResult = await aplicarRateLimit(
|
||||
ctx,
|
||||
'usuario',
|
||||
args.usuarioId,
|
||||
'atualizarReputacaoIndicador'
|
||||
);
|
||||
if (!rateLimitResult.permitido) {
|
||||
throw new Error(rateLimitResult.motivo ?? 'Rate limit excedido');
|
||||
}
|
||||
@@ -1075,7 +1091,8 @@ export const configurarRegraPorta = mutation({
|
||||
}),
|
||||
handler: async (ctx, args) => {
|
||||
const agora = Date.now();
|
||||
const expiraEm = args.temporario && args.duracaoSegundos ? agora + args.duracaoSegundos * 1000 : undefined;
|
||||
const expiraEm =
|
||||
args.temporario && args.duracaoSegundos ? agora + args.duracaoSegundos * 1000 : undefined;
|
||||
|
||||
if (args.regraId) {
|
||||
await ctx.db.patch(args.regraId, {
|
||||
@@ -1172,7 +1189,14 @@ export const registrarAcaoIncidente = mutation({
|
||||
tipo: acaoIncidenteValidator,
|
||||
origem: acaoOrigemValidator,
|
||||
executadoPor: v.optional(v.id('usuarios')),
|
||||
status: v.optional(v.union(v.literal('pendente'), v.literal('executando'), v.literal('concluido'), v.literal('falhou'))),
|
||||
status: v.optional(
|
||||
v.union(
|
||||
v.literal('pendente'),
|
||||
v.literal('executando'),
|
||||
v.literal('concluido'),
|
||||
v.literal('falhou')
|
||||
)
|
||||
),
|
||||
detalhes: v.optional(v.string()),
|
||||
resultado: v.optional(v.string()),
|
||||
relacionadoA: v.optional(v.id('ipReputation'))
|
||||
@@ -1338,9 +1362,7 @@ export const processarRelatorioSegurancaInternal = internalMutation({
|
||||
const eventos = await ctx.db
|
||||
.query('securityEvents')
|
||||
.withIndex('by_timestamp', (q) =>
|
||||
q
|
||||
.gte('timestamp', relatorio.filtros.dataInicio)
|
||||
.lte('timestamp', relatorio.filtros.dataFim)
|
||||
q.gte('timestamp', relatorio.filtros.dataInicio).lte('timestamp', relatorio.filtros.dataFim)
|
||||
)
|
||||
.collect();
|
||||
|
||||
@@ -1350,14 +1372,14 @@ export const processarRelatorioSegurancaInternal = internalMutation({
|
||||
relatorio.filtros.severidades.length > 0 &&
|
||||
!relatorio.filtros.severidades.includes(evento.severidade)
|
||||
) {
|
||||
return false;
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
relatorio.filtros.tiposAtaque &&
|
||||
relatorio.filtros.tiposAtaque.length > 0 &&
|
||||
!relatorio.filtros.tiposAtaque.includes(evento.tipoAtaque)
|
||||
) {
|
||||
return false;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
@@ -1509,7 +1531,10 @@ export const dispararAlertasInternos = internalMutation({
|
||||
const usuariosNotificados: Id<'usuarios'>[] = [];
|
||||
|
||||
for (const role of rolesTi) {
|
||||
const membros = await ctx.db.query('usuarios').withIndex('by_role', (q) => q.eq('roleId', role._id)).collect();
|
||||
const membros = await ctx.db
|
||||
.query('usuarios')
|
||||
.withIndex('by_role', (q) => q.eq('roleId', role._id))
|
||||
.collect();
|
||||
for (const usuario of membros) {
|
||||
usuariosNotificados.push(usuario._id);
|
||||
}
|
||||
@@ -1602,7 +1627,9 @@ async function aplicarRateLimit(
|
||||
): Promise<{ permitido: boolean; motivo?: string; retryAfter?: number }> {
|
||||
const configs = await ctx.db
|
||||
.query('rateLimitConfig')
|
||||
.withIndex('by_tipo_identificador', (q) => q.eq('tipo', tipo).eq('identificador', identificador))
|
||||
.withIndex('by_tipo_identificador', (q) =>
|
||||
q.eq('tipo', tipo).eq('identificador', identificador)
|
||||
)
|
||||
.filter((q) => q.eq(q.field('ativo'), true))
|
||||
.collect();
|
||||
|
||||
@@ -1611,7 +1638,9 @@ async function aplicarRateLimit(
|
||||
// Verificar configuração global
|
||||
const globalConfigs = await ctx.db
|
||||
.query('rateLimitConfig')
|
||||
.withIndex('by_tipo_identificador', (q) => q.eq('tipo', 'global').eq('identificador', 'global'))
|
||||
.withIndex('by_tipo_identificador', (q) =>
|
||||
q.eq('tipo', 'global').eq('identificador', 'global')
|
||||
)
|
||||
.filter((q) => q.eq(q.field('ativo'), true))
|
||||
.collect();
|
||||
|
||||
@@ -1626,10 +1655,11 @@ async function aplicarRateLimit(
|
||||
|
||||
// Converter janelaSegundos para período do rate-limiter
|
||||
const periodo = config.janelaSegundos * SECOND;
|
||||
|
||||
|
||||
// Determinar estratégia baseada na configuração
|
||||
// O rate-limiter suporta apenas 'token bucket' e 'fixed window'
|
||||
const kind: 'token bucket' | 'fixed window' = config.estrategia === 'token_bucket' ? 'token bucket' : 'fixed window';
|
||||
const kind: 'token bucket' | 'fixed window' =
|
||||
config.estrategia === 'token_bucket' ? 'token bucket' : 'fixed window';
|
||||
|
||||
// Criar namespace único para este rate limit
|
||||
const namespace = `${tipo}:${identificador}:${endpoint ?? 'default'}`;
|
||||
@@ -1643,7 +1673,10 @@ async function aplicarRateLimit(
|
||||
period: periodo,
|
||||
...(config.estrategia === 'token_bucket' ? { capacity: config.limite } : {})
|
||||
}
|
||||
} as Record<string, { kind: 'token bucket' | 'fixed window'; rate: number; period: number; capacity?: number }>;
|
||||
} as Record<
|
||||
string,
|
||||
{ kind: 'token bucket' | 'fixed window'; rate: number; period: number; capacity?: number }
|
||||
>;
|
||||
|
||||
const rateLimiter = new RateLimiter(components.rateLimiter, rateLimiterConfig);
|
||||
|
||||
@@ -1654,7 +1687,7 @@ async function aplicarRateLimit(
|
||||
|
||||
if (!result.ok) {
|
||||
const retryAfter = result.retryAfter ?? periodo;
|
||||
|
||||
|
||||
if (config.acaoExcedido === 'bloquear') {
|
||||
return {
|
||||
permitido: false,
|
||||
@@ -1688,7 +1721,12 @@ export const criarConfigRateLimit = mutation({
|
||||
args: {
|
||||
usuarioId: v.id('usuarios'),
|
||||
nome: v.string(),
|
||||
tipo: v.union(v.literal('ip'), v.literal('usuario'), v.literal('endpoint'), v.literal('global')),
|
||||
tipo: v.union(
|
||||
v.literal('ip'),
|
||||
v.literal('usuario'),
|
||||
v.literal('endpoint'),
|
||||
v.literal('global')
|
||||
),
|
||||
identificador: v.optional(v.string()),
|
||||
limite: v.number(),
|
||||
janelaSegundos: v.number(),
|
||||
@@ -1737,13 +1775,11 @@ export const atualizarConfigRateLimit = mutation({
|
||||
limite: v.optional(v.number()),
|
||||
janelaSegundos: v.optional(v.number()),
|
||||
estrategia: v.optional(
|
||||
v.union(
|
||||
v.literal('fixed_window'),
|
||||
v.literal('sliding_window'),
|
||||
v.literal('token_bucket')
|
||||
)
|
||||
v.union(v.literal('fixed_window'), v.literal('sliding_window'), v.literal('token_bucket'))
|
||||
),
|
||||
acaoExcedido: v.optional(
|
||||
v.union(v.literal('bloquear'), v.literal('throttle'), v.literal('alertar'))
|
||||
),
|
||||
acaoExcedido: v.optional(v.union(v.literal('bloquear'), v.literal('throttle'), v.literal('alertar'))),
|
||||
bloqueioTemporarioSegundos: v.optional(v.number()),
|
||||
ativo: v.optional(v.boolean()),
|
||||
prioridade: v.optional(v.number()),
|
||||
@@ -1794,7 +1830,9 @@ export const atualizarConfigRateLimit = mutation({
|
||||
|
||||
export const listarConfigsRateLimit = query({
|
||||
args: {
|
||||
tipo: v.optional(v.union(v.literal('ip'), v.literal('usuario'), v.literal('endpoint'), v.literal('global'))),
|
||||
tipo: v.optional(
|
||||
v.union(v.literal('ip'), v.literal('usuario'), v.literal('endpoint'), v.literal('global'))
|
||||
),
|
||||
ativo: v.optional(v.boolean()),
|
||||
limit: v.optional(v.number())
|
||||
},
|
||||
@@ -1802,7 +1840,12 @@ export const listarConfigsRateLimit = query({
|
||||
v.object({
|
||||
_id: v.id('rateLimitConfig'),
|
||||
nome: v.string(),
|
||||
tipo: v.union(v.literal('ip'), v.literal('usuario'), v.literal('endpoint'), v.literal('global')),
|
||||
tipo: v.union(
|
||||
v.literal('ip'),
|
||||
v.literal('usuario'),
|
||||
v.literal('endpoint'),
|
||||
v.literal('global')
|
||||
),
|
||||
identificador: v.optional(v.string()),
|
||||
limite: v.number(),
|
||||
janelaSegundos: v.number(),
|
||||
@@ -1882,8 +1925,12 @@ export const analisarRequisicaoHTTP = mutation({
|
||||
args.url,
|
||||
args.method,
|
||||
args.body ?? '',
|
||||
Object.entries(args.queryParams ?? {}).map(([k, v]) => `${k}=${v}`).join('&'),
|
||||
Object.entries(args.headers ?? {}).map(([k, v]) => `${k}:${v}`).join('\n'),
|
||||
Object.entries(args.queryParams ?? {})
|
||||
.map(([k, v]) => `${k}=${v}`)
|
||||
.join('&'),
|
||||
Object.entries(args.headers ?? {})
|
||||
.map(([k, v]) => `${k}:${v}`)
|
||||
.join('\n'),
|
||||
args.userAgent ?? ''
|
||||
].join('\n');
|
||||
|
||||
@@ -1904,7 +1951,8 @@ export const analisarRequisicaoHTTP = mutation({
|
||||
|
||||
// Permitir que o chamador informe o destino/protocolo via query string em cenários de dev/teste
|
||||
const destinoIp =
|
||||
(args.queryParams && (args.queryParams['dst'] || args.queryParams['dest'] || args.queryParams['destino'])) ||
|
||||
(args.queryParams &&
|
||||
(args.queryParams['dst'] || args.queryParams['dest'] || args.queryParams['destino'])) ||
|
||||
undefined;
|
||||
const protocolo = (args.queryParams && (args.queryParams['proto'] as string)) || 'http';
|
||||
|
||||
@@ -1922,9 +1970,11 @@ export const analisarRequisicaoHTTP = mutation({
|
||||
protocolo,
|
||||
transporte: 'tcp',
|
||||
detectadoPor: 'analisador_http_automatico',
|
||||
fingerprint: args.userAgent ? {
|
||||
userAgent: args.userAgent
|
||||
} : undefined,
|
||||
fingerprint: args.userAgent
|
||||
? {
|
||||
userAgent: args.userAgent
|
||||
}
|
||||
: undefined,
|
||||
destinoIp: destinoIp ?? undefined,
|
||||
tags: ['detecção_automática', 'http', tipoAtaque],
|
||||
atualizadoEm: agora
|
||||
@@ -1978,19 +2028,13 @@ export const detectarBruteForce = internalMutation({
|
||||
tentativasFalhas = await ctx.db
|
||||
.query('logsLogin')
|
||||
.withIndex('by_ip', (q) => q.eq('ipAddress', args.ipAddress))
|
||||
.filter((q) =>
|
||||
q.gte(q.field('timestamp'), dataLimite) &&
|
||||
q.eq(q.field('sucesso'), false)
|
||||
)
|
||||
.filter((q) => q.gte(q.field('timestamp'), dataLimite) && q.eq(q.field('sucesso'), false))
|
||||
.collect();
|
||||
} else if (args.usuarioId) {
|
||||
tentativasFalhas = await ctx.db
|
||||
.query('logsLogin')
|
||||
.withIndex('by_usuario', (q) => q.eq('usuarioId', args.usuarioId))
|
||||
.filter((q) =>
|
||||
q.gte(q.field('timestamp'), dataLimite) &&
|
||||
q.eq(q.field('sucesso'), false)
|
||||
)
|
||||
.filter((q) => q.gte(q.field('timestamp'), dataLimite) && q.eq(q.field('sucesso'), false))
|
||||
.collect();
|
||||
} else {
|
||||
// Buscar todas as tentativas falhas na janela
|
||||
@@ -2026,7 +2070,8 @@ export const detectarBruteForce = internalMutation({
|
||||
const eventosIds: Id<'securityEvents'>[] = [];
|
||||
|
||||
for (const { ip, count } of ipsSuspeitos) {
|
||||
const severidade: SeveridadeSeguranca = count >= 8 ? 'alto' : count >= 5 ? 'moderado' : 'baixo';
|
||||
const severidade: SeveridadeSeguranca =
|
||||
count >= 8 ? 'alto' : count >= 5 ? 'moderado' : 'baixo';
|
||||
const referencia = `brute_force_${ip}_${Date.now()}`;
|
||||
const agora = Date.now();
|
||||
|
||||
@@ -2058,10 +2103,12 @@ export const detectarBruteForce = internalMutation({
|
||||
'ip',
|
||||
delta,
|
||||
severidade,
|
||||
severidade === 'alto' ? {
|
||||
blacklist: true,
|
||||
bloqueadoAte: agora + (60 * 60 * 1000) // Bloquear por 1 hora
|
||||
} : undefined
|
||||
severidade === 'alto'
|
||||
? {
|
||||
blacklist: true,
|
||||
bloqueadoAte: agora + 60 * 60 * 1000 // Bloquear por 1 hora
|
||||
}
|
||||
: undefined
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2109,13 +2156,7 @@ export const criarEventosTeste = mutation({
|
||||
];
|
||||
|
||||
// IPs de teste
|
||||
const ipsTeste = [
|
||||
'192.168.1.100',
|
||||
'10.0.0.50',
|
||||
'172.16.0.25',
|
||||
'203.0.113.42',
|
||||
'198.51.100.15'
|
||||
];
|
||||
const ipsTeste = ['192.168.1.100', '10.0.0.50', '172.16.0.25', '203.0.113.42', '198.51.100.15'];
|
||||
|
||||
for (let i = 0; i < quantidade; i++) {
|
||||
const tipoAtaque = tiposAtaque[i % tiposAtaque.length];
|
||||
@@ -2124,7 +2165,7 @@ export const criarEventosTeste = mutation({
|
||||
|
||||
const eventoId = await ctx.db.insert('securityEvents', {
|
||||
referencia,
|
||||
timestamp: agora - (i * 60000), // Espaçar eventos em 1 minuto
|
||||
timestamp: agora - i * 60000, // Espaçar eventos em 1 minuto
|
||||
tipoAtaque: tipoAtaque.tipo,
|
||||
severidade: tipoAtaque.severidade,
|
||||
status: statusInicial(tipoAtaque.severidade),
|
||||
@@ -2140,7 +2181,7 @@ export const criarEventosTeste = mutation({
|
||||
pps: Math.floor(Math.random() * 50000)
|
||||
},
|
||||
tags: ['teste', 'validação', tipoAtaque.tipo],
|
||||
atualizadoEm: agora - (i * 60000)
|
||||
atualizadoEm: agora - i * 60000
|
||||
});
|
||||
|
||||
eventosIds.push(eventoId);
|
||||
@@ -2153,9 +2194,11 @@ export const criarEventosTeste = mutation({
|
||||
'ip',
|
||||
delta,
|
||||
tipoAtaque.severidade,
|
||||
tipoAtaque.severidade === 'critico' || tipoAtaque.severidade === 'alto' ? {
|
||||
blacklist: true
|
||||
} : undefined
|
||||
tipoAtaque.severidade === 'critico' || tipoAtaque.severidade === 'alto'
|
||||
? {
|
||||
blacklist: true
|
||||
}
|
||||
: undefined
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2208,7 +2251,8 @@ export const monitorarLogsLogin = internalMutation({
|
||||
// Registrar eventos para cada IP suspeito
|
||||
let ipsBloqueados = 0;
|
||||
for (const { ip, count } of ipsSuspeitos) {
|
||||
const severidade: SeveridadeSeguranca = count >= 8 ? 'alto' : count >= 5 ? 'moderado' : 'baixo';
|
||||
const severidade: SeveridadeSeguranca =
|
||||
count >= 8 ? 'alto' : count >= 5 ? 'moderado' : 'baixo';
|
||||
const referencia = `brute_force_${ip}_${Date.now()}`;
|
||||
const agora = Date.now();
|
||||
|
||||
@@ -2238,10 +2282,12 @@ export const monitorarLogsLogin = internalMutation({
|
||||
'ip',
|
||||
delta,
|
||||
severidade,
|
||||
severidade === 'alto' ? {
|
||||
blacklist: true,
|
||||
bloqueadoAte: agora + (60 * 60 * 1000)
|
||||
} : undefined
|
||||
severidade === 'alto'
|
||||
? {
|
||||
blacklist: true,
|
||||
bloqueadoAte: agora + 60 * 60 * 1000
|
||||
}
|
||||
: undefined
|
||||
);
|
||||
|
||||
if (severidade === 'alto') {
|
||||
@@ -2302,7 +2348,12 @@ export const seedRateLimitDev = mutation({
|
||||
const existing = await ctx.db
|
||||
.query('rateLimitConfig')
|
||||
.withIndex('by_tipo_identificador', (q) =>
|
||||
q.eq('tipo', params.tipo).eq('identificador', params.identificador ?? (params.tipo === 'global' ? 'global' : undefined)),
|
||||
q
|
||||
.eq('tipo', params.tipo)
|
||||
.eq(
|
||||
'identificador',
|
||||
params.identificador ?? (params.tipo === 'global' ? 'global' : undefined)
|
||||
)
|
||||
)
|
||||
.collect();
|
||||
const agora = Date.now();
|
||||
@@ -2315,9 +2366,9 @@ export const seedRateLimitDev = mutation({
|
||||
estrategia: params.estrategia,
|
||||
acaoExcedido: params.acaoExcedido,
|
||||
ativo: true,
|
||||
prioridade: params.prioridade ?? (doc.prioridade ?? 0),
|
||||
prioridade: params.prioridade ?? doc.prioridade ?? 0,
|
||||
atualizadoEm: agora,
|
||||
notas: params.notas,
|
||||
notas: params.notas
|
||||
});
|
||||
} else {
|
||||
await ctx.db.insert('rateLimitConfig', {
|
||||
@@ -2420,4 +2471,3 @@ export const deletarRegraPorta = mutation({
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user