From fb22f82ce68213227fd949e3b62b26bf1a8d6746 Mon Sep 17 00:00:00 2001 From: killer-cf Date: Fri, 9 Jan 2026 16:24:38 -0300 Subject: [PATCH 1/7] chore: update package manager to bun@1.3.5 and streamline Dockerfile by removing unnecessary user creation and ownership settings, enhancing build efficiency --- apps/web/Dockerfile | 14 +- .../gestao-ausencias/+page.svelte | 24 +- .../recursos-humanos/ausencias/+page.svelte | 32 +- .../funcionarios/+page.svelte | 2 +- .../enderecos-marcacao/+page.svelte | 8 +- .../gestao-ausencias/+page.svelte | 34 +- .../notificacoes/templates/novo/+page.svelte | 341 +++++++++--------- package.json | 2 +- 8 files changed, 237 insertions(+), 220 deletions(-) diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile index 71f9ef7..073d6e9 100644 --- a/apps/web/Dockerfile +++ b/apps/web/Dockerfile @@ -35,21 +35,17 @@ FROM oven/bun:1-slim AS production # Set working directory to match builder structure WORKDIR /app -# Create non-root user -RUN addgroup --system --gid 1001 sveltekit -RUN adduser --system --uid 1001 sveltekit - # Copy root node_modules (contains hoisted dependencies) -COPY --from=builder --chown=sveltekit:sveltekit /app/node_modules ./node_modules +COPY --from=builder /app/node_modules ./node_modules # Copy built application and workspace files -COPY --from=builder --chown=sveltekit:sveltekit /app/apps/web/build ./apps/web/build -COPY --from=builder --chown=sveltekit:sveltekit /app/apps/web/package.json ./apps/web/package.json +COPY --from=builder /app/apps/web/build ./apps/web/build +COPY --from=builder /app/apps/web/package.json ./apps/web/package.json # Copy workspace node_modules (contains symlinks to root node_modules) -COPY --from=builder --chown=sveltekit:sveltekit /app/apps/web/node_modules ./apps/web/node_modules +COPY --from=builder /app/apps/web/node_modules ./apps/web/node_modules # Copy any additional files needed for runtime -COPY --from=builder --chown=sveltekit:sveltekit /app/apps/web/static ./apps/web/static +COPY --from=builder /app/apps/web/static ./apps/web/static # Switch to non-root user USER sveltekit diff --git a/apps/web/src/routes/(dashboard)/gestao-pessoas/gestao-ausencias/+page.svelte b/apps/web/src/routes/(dashboard)/gestao-pessoas/gestao-ausencias/+page.svelte index bad2544..16875cf 100644 --- a/apps/web/src/routes/(dashboard)/gestao-pessoas/gestao-ausencias/+page.svelte +++ b/apps/web/src/routes/(dashboard)/gestao-pessoas/gestao-ausencias/+page.svelte @@ -5,7 +5,6 @@ import { goto } from '$app/navigation'; import { resolve } from '$app/paths'; import AprovarAusencias from '$lib/components/AprovarAusencias.svelte'; - import type { Id } from '@sgse-app/backend/convex/_generated/dataModel'; import { parseLocalDate } from '$lib/utils/datas'; import jsPDF from 'jspdf'; import autoTable from 'jspdf-autotable'; @@ -15,13 +14,14 @@ import logoGovPE from '$lib/assets/logo_governo_PE.png'; import { FileDown, FileSpreadsheet } from 'lucide-svelte'; import { toast } from 'svelte-sonner'; + import { SvelteDate } from 'svelte/reactivity'; const client = useConvexClient(); const currentUser = useQuery(api.auth.getCurrentUser, {}); // Buscar TODAS as solicitações de ausências const todasAusenciasQuery = useQuery(api.ausencias.listarTodas, {}); - + // Buscar funcionários para filtro const funcionariosQuery = useQuery(api.funcionarios.getAll, {}); @@ -34,7 +34,9 @@ const ausencias = $derived(todasAusenciasQuery?.data || []); const funcionarios = $derived( - Array.isArray(funcionariosQuery?.data) ? funcionariosQuery.data : funcionariosQuery?.data?.data || [] + Array.isArray(funcionariosQuery?.data) + ? funcionariosQuery.data + : funcionariosQuery?.data?.data || [] ); // Filtrar solicitações @@ -42,26 +44,26 @@ ausencias.filter((a) => { // Filtro de status if (filtroStatus !== 'todos' && a.status !== filtroStatus) return false; - + // Filtro por funcionário if (filtroFuncionario) { if (a.funcionario?._id !== filtroFuncionario) return false; } - + // Filtro por período if (filtroPeriodoInicio) { const inicioFiltro = new Date(filtroPeriodoInicio); const inicioAusencia = parseLocalDate(a.dataInicio); if (inicioAusencia < inicioFiltro) return false; } - + if (filtroPeriodoFim) { - const fimFiltro = new Date(filtroPeriodoFim); + const fimFiltro = new SvelteDate(filtroPeriodoFim); fimFiltro.setHours(23, 59, 59, 999); // Incluir o dia inteiro const fimAusencia = parseLocalDate(a.dataFim); if (fimAusencia > fimFiltro) return false; } - + return true; }) ); @@ -690,7 +692,7 @@ bind:value={filtroFuncionario} > - {#each funcionarios as funcionario} + {#each funcionarios as funcionario (funcionario._id)} {/each} @@ -761,7 +763,7 @@ - {#each ausenciasFiltradas as ausencia} + {#each ausenciasFiltradas as ausencia (ausencia._id)} {ausencia.funcionario?.nome || 'N/A'} @@ -769,7 +771,7 @@ {#if ausencia.time}
import { api } from '@sgse-app/backend/convex/_generated/api'; - import type { Id } from '@sgse-app/backend/convex/_generated/dataModel'; import { useConvexClient, useQuery } from 'convex-svelte'; import { goto } from '$app/navigation'; import { resolve } from '$app/paths'; import AprovarAusencias from '$lib/components/AprovarAusencias.svelte'; import type { Id } from '@sgse-app/backend/convex/_generated/dataModel'; - import { Clock, ArrowLeft, FileText, CheckCircle, XCircle, Info, Eye, FileDown, FileSpreadsheet } from 'lucide-svelte'; + import { + Clock, + ArrowLeft, + FileText, + CheckCircle, + XCircle, + Info, + Eye, + FileDown, + FileSpreadsheet + } from 'lucide-svelte'; import { parseLocalDate } from '$lib/utils/datas'; import jsPDF from 'jspdf'; import autoTable from 'jspdf-autotable'; @@ -15,13 +24,14 @@ import ExcelJS from 'exceljs'; import logoGovPE from '$lib/assets/logo_governo_PE.png'; import { toast } from 'svelte-sonner'; + import { SvelteDate } from 'svelte/reactivity'; const client = useConvexClient(); const currentUser = useQuery(api.auth.getCurrentUser, {}); // Buscar TODAS as solicitações de ausências (Dashboard RH) const todasAusenciasQuery = useQuery(api.ausencias.listarTodas, {}); - + // Buscar funcionários para filtro const funcionariosQuery = useQuery(api.funcionarios.getAll, {}); @@ -34,7 +44,9 @@ const ausencias = $derived(todasAusenciasQuery?.data || []); const funcionarios = $derived( - Array.isArray(funcionariosQuery?.data) ? funcionariosQuery.data : funcionariosQuery?.data?.data || [] + Array.isArray(funcionariosQuery?.data) + ? funcionariosQuery.data + : funcionariosQuery?.data?.data || [] ); // Filtrar solicitações @@ -42,26 +54,26 @@ ausencias.filter((a) => { // Filtro de status if (filtroStatus !== 'todos' && a.status !== filtroStatus) return false; - + // Filtro por funcionário if (filtroFuncionario) { if (a.funcionario?._id !== filtroFuncionario) return false; } - + // Filtro por período if (filtroPeriodoInicio) { const inicioFiltro = new Date(filtroPeriodoInicio); const inicioAusencia = parseLocalDate(a.dataInicio); if (inicioAusencia < inicioFiltro) return false; } - + if (filtroPeriodoFim) { - const fimFiltro = new Date(filtroPeriodoFim); + const fimFiltro = new SvelteDate(filtroPeriodoFim); fimFiltro.setHours(23, 59, 59, 999); // Incluir o dia inteiro const fimAusencia = parseLocalDate(a.dataFim); if (fimAusencia > fimFiltro) return false; } - + return true; }) ); @@ -679,7 +691,7 @@ {#if ausencia.time}
); + let funcionarioId = $derived(page.params.funcionarioId as Id<'funcionarios'>); // Queries const funcionarioQuery = useQuery( diff --git a/apps/web/src/routes/(dashboard)/secretaria-executiva/gestao-ausencias/+page.svelte b/apps/web/src/routes/(dashboard)/secretaria-executiva/gestao-ausencias/+page.svelte index 03d1a5e..1a1b006 100644 --- a/apps/web/src/routes/(dashboard)/secretaria-executiva/gestao-ausencias/+page.svelte +++ b/apps/web/src/routes/(dashboard)/secretaria-executiva/gestao-ausencias/+page.svelte @@ -5,8 +5,17 @@ import { goto } from '$app/navigation'; import { resolve } from '$app/paths'; import AprovarAusencias from '$lib/components/AprovarAusencias.svelte'; - import type { Id } from '@sgse-app/backend/convex/_generated/dataModel'; - import { Clock, ArrowLeft, FileText, CheckCircle, XCircle, Info, Eye, FileDown, FileSpreadsheet } from 'lucide-svelte'; + import { + Clock, + ArrowLeft, + FileText, + CheckCircle, + XCircle, + Info, + Eye, + FileDown, + FileSpreadsheet + } from 'lucide-svelte'; import { parseLocalDate } from '$lib/utils/datas'; import jsPDF from 'jspdf'; import autoTable from 'jspdf-autotable'; @@ -15,13 +24,14 @@ import ExcelJS from 'exceljs'; import logoGovPE from '$lib/assets/logo_governo_PE.png'; import { toast } from 'svelte-sonner'; + import { SvelteDate } from 'svelte/reactivity'; const client = useConvexClient(); const currentUser = useQuery(api.auth.getCurrentUser, {}); // Buscar TODAS as solicitações de ausências const todasAusenciasQuery = useQuery(api.ausencias.listarTodas, {}); - + // Buscar funcionários para filtro const funcionariosQuery = useQuery(api.funcionarios.getAll, {}); @@ -34,7 +44,9 @@ let ausencias = $derived(todasAusenciasQuery?.data || []); let funcionarios = $derived( - Array.isArray(funcionariosQuery?.data) ? funcionariosQuery.data : funcionariosQuery?.data?.data || [] + Array.isArray(funcionariosQuery?.data) + ? funcionariosQuery.data + : funcionariosQuery?.data?.data || [] ); // Filtrar solicitações @@ -42,26 +54,26 @@ ausencias.filter((a) => { // Filtro de status if (filtroStatus !== 'todos' && a.status !== filtroStatus) return false; - + // Filtro por funcionário if (filtroFuncionario) { if (a.funcionario?._id !== filtroFuncionario) return false; } - + // Filtro por período if (filtroPeriodoInicio) { const inicioFiltro = new Date(filtroPeriodoInicio); const inicioAusencia = parseLocalDate(a.dataInicio); if (inicioAusencia < inicioFiltro) return false; } - + if (filtroPeriodoFim) { - const fimFiltro = new Date(filtroPeriodoFim); + const fimFiltro = new SvelteDate(filtroPeriodoFim); fimFiltro.setHours(23, 59, 59, 999); // Incluir o dia inteiro const fimAusencia = parseLocalDate(a.dataFim); if (fimAusencia > fimFiltro) return false; } - + return true; }) ); @@ -612,7 +624,7 @@ bind:value={filtroFuncionario} > - {#each funcionarios as funcionario} + {#each funcionarios as funcionario (funcionario._id)} {/each} @@ -671,7 +683,7 @@ - {#each ausenciasFiltradas as ausencia} + {#each ausenciasFiltradas as ausencia (ausencia._id)} {ausencia.funcionario?.nome || 'N/A'} diff --git a/apps/web/src/routes/(dashboard)/ti/notificacoes/templates/novo/+page.svelte b/apps/web/src/routes/(dashboard)/ti/notificacoes/templates/novo/+page.svelte index 3ec669b..57096b4 100644 --- a/apps/web/src/routes/(dashboard)/ti/notificacoes/templates/novo/+page.svelte +++ b/apps/web/src/routes/(dashboard)/ti/notificacoes/templates/novo/+page.svelte @@ -7,58 +7,54 @@ import { resolve } from '$app/paths'; import { FileText } from 'lucide-svelte'; -const client = useConvexClient(); -const currentUser = useQuery( - api.auth.getCurrentUser as FunctionReference<"query">, -); + const client = useConvexClient(); + const currentUser = useQuery(api.auth.getCurrentUser as FunctionReference<'query'>); -let codigo = $state(""); -let nome = $state(""); -let titulo = $state(""); -let corpo = $state(""); -let categoria = $state<"email" | "chat" | "ambos">("email"); -let variaveisTexto = $state(""); -let tagsTexto = $state(""); -let criando = $state(false); -let mensagem = $state<{ - tipo: "success" | "error" | "info"; - texto: string; -} | null>(null); + let codigo = $state(''); + let nome = $state(''); + let titulo = $state(''); + let corpo = $state(''); + let categoria = $state<'email' | 'chat' | 'ambos'>('email'); + let variaveisTexto = $state(''); + let tagsTexto = $state(''); + let criando = $state(false); + let mensagem = $state<{ + tipo: 'success' | 'error' | 'info'; + texto: string; + } | null>(null); -function mostrarMensagem(tipo: "success" | "error" | "info", texto: string) { - mensagem = { tipo, texto }; - setTimeout(() => { - mensagem = null; - }, 5000); -} - -function parseLista(input: string): string[] { - return input - .split(/[;,\n]/) - .map((v) => v.trim()) - .filter((v) => v.length > 0); -} - -async function salvar() { - if (!currentUser.data) { - mostrarMensagem("error", "Usuário não autenticado."); - return; + function mostrarMensagem(tipo: 'success' | 'error' | 'info', texto: string) { + mensagem = { tipo, texto }; + setTimeout(() => { + mensagem = null; + }, 5000); } - if (!codigo.trim() || !nome.trim() || !titulo.trim() || !corpo.trim()) { - mostrarMensagem("error", "Preencha todos os campos obrigatórios."); - return; + function parseLista(input: string): string[] { + return input + .split(/[;,\n]/) + .map((v) => v.trim()) + .filter((v) => v.length > 0); } - const codigoNormalizado = codigo.trim().toUpperCase().replace(/\s+/g, "_"); - const variaveis = parseLista(variaveisTexto); - const tags = parseLista(tagsTexto); + async function salvar() { + if (!currentUser.data) { + mostrarMensagem('error', 'Usuário não autenticado.'); + return; + } - try { - criando = true; - const resultado = await client.mutation( - api.templatesMensagens.criarTemplate, - { + if (!codigo.trim() || !nome.trim() || !titulo.trim() || !corpo.trim()) { + mostrarMensagem('error', 'Preencha todos os campos obrigatórios.'); + return; + } + + const codigoNormalizado = codigo.trim().toUpperCase().replace(/\s+/g, '_'); + const variaveis = parseLista(variaveisTexto); + const tags = parseLista(tagsTexto); + + try { + criando = true; + const resultado = await client.mutation(api.templatesMensagens.criarTemplate, { codigo: codigoNormalizado, nome: nome.trim(), titulo: titulo.trim(), @@ -66,39 +62,38 @@ async function salvar() { variaveis, categoria, tags, - criadoPorId: currentUser.data._id as Id<"usuarios">, - }, - ); + criadoPorId: currentUser.data._id as Id<'usuarios'> + }); - if (resultado.sucesso) { - mostrarMensagem("success", "Template criado com sucesso!"); - await goto(resolve("/ti/notificacoes/templates")); - } else { - mostrarMensagem("error", resultado.erro || "Erro ao criar template."); + if (resultado.sucesso) { + mostrarMensagem('success', 'Template criado com sucesso!'); + await goto(resolve('/ti/notificacoes/templates')); + } else { + mostrarMensagem('error', resultado.erro || 'Erro ao criar template.'); + } + } catch (error) { + const erro = error instanceof Error ? error.message : 'Erro desconhecido'; + mostrarMensagem('error', `Erro ao criar template: ${erro}`); + } finally { + criando = false; } - } catch (error) { - const erro = error instanceof Error ? error.message : "Erro desconhecido"; - mostrarMensagem("error", `Erro ao criar template: ${erro}`); - } finally { - criando = false; } -}
-
+

Novo Template

- Defina o texto base que será usado em chat e na versão - HTML de email com o estilo padrão do SGSE. + Defina o texto base que será usado em chat e na + versão HTML de email com o estilo padrão do SGSE.

@@ -125,129 +120,131 @@ async function salvar() {
{/if} -
+
-
+
+
+ + +
+
+ + +
+
+
-
+
-
-
- -
- - -
- -
- - - -
- -
- - - -
- -
-
- - -
-
- - -
-
-
- Cancelar - +
+ + +
+ + Este texto será usado diretamente nas mensagens de chat. Para email, o sistema gera automaticamente um + layout HTML padronizado com logo e assinatura. + +
+
+ +
+
+ + + +
+
+ + +
+
+ +
+ Cancelar + +
diff --git a/package.json b/package.json index 3f6625b..26912c9 100644 --- a/package.json +++ b/package.json @@ -48,5 +48,5 @@ "svelte-chartjs": "^3.1.5", "svelte-sonner": "^1.0.7" }, - "packageManager": "bun@1.3.4" + "packageManager": "bun@1.3.5" } -- 2.49.1 From 664d90c2e0ea6cad9831269b0f635b5a3111e900 Mon Sep 17 00:00:00 2001 From: deyvisonwanderley Date: Mon, 12 Jan 2026 04:34:00 -0300 Subject: [PATCH 2/7] feat: implement security enhancements for Jitsi integration, including JWT token generation and automatic blocking of detected attacks, improving system resilience and user authentication --- apps/web/src/hooks.server.ts | 86 ++ .../src/lib/components/call/CallWindow.svelte | 102 +- .../src/lib/components/chat/ChatWindow.svelte | 18 + .../ti/AttackCapabilitiesSection.svelte | 152 +++ .../components/ti/AttackDetailsModal.svelte | 187 ++++ .../components/ti/AttackTypeTooltip.svelte | 80 ++ .../components/ti/CybersecurityWizcard.svelte | 580 +++++++++-- apps/web/src/lib/data/attackCapabilities.ts | 971 ++++++++++++++++++ apps/web/src/lib/utils/jitsi.ts | 81 ++ .../routes/(dashboard)/perfil/+page.svelte | 153 ++- .../ti/cibersecurity/+page.server.ts | 19 + .../ti/configuracoes-jitsi/+page.svelte | 925 +++++++++++++++-- .../routes/(dashboard)/ti/times/+page.svelte | 195 +++- docs/JITSI_CONFIGURACAO.md | 621 +++++++++++ packages/backend/convex/auth.ts | 2 +- packages/backend/convex/auth/utils.ts | 16 + packages/backend/convex/configuracaoJitsi.ts | 747 +++++++++++++- packages/backend/convex/http.ts | 56 +- packages/backend/convex/pontos.ts | 15 +- packages/backend/convex/security.ts | 457 ++++++++- packages/backend/convex/tables/security.ts | 17 +- packages/backend/convex/tables/system.ts | 9 +- packages/backend/convex/times.ts | 19 +- packages/backend/package.json | 2 +- scripts/REVISAO_CIBERSECURITY.md | 166 +++ scripts/configurar-jitsi.sh | 526 ++++++++++ scripts/teste_seguranca.py | 301 +++++- 27 files changed, 6174 insertions(+), 329 deletions(-) create mode 100644 apps/web/src/lib/components/ti/AttackCapabilitiesSection.svelte create mode 100644 apps/web/src/lib/components/ti/AttackDetailsModal.svelte create mode 100644 apps/web/src/lib/components/ti/AttackTypeTooltip.svelte create mode 100644 apps/web/src/lib/data/attackCapabilities.ts create mode 100644 apps/web/src/routes/(dashboard)/ti/cibersecurity/+page.server.ts create mode 100644 docs/JITSI_CONFIGURACAO.md create mode 100644 scripts/REVISAO_CIBERSECURITY.md create mode 100755 scripts/configurar-jitsi.sh diff --git a/apps/web/src/hooks.server.ts b/apps/web/src/hooks.server.ts index a04f98e..d9ee317 100644 --- a/apps/web/src/hooks.server.ts +++ b/apps/web/src/hooks.server.ts @@ -6,6 +6,92 @@ import { api } from '@sgse-app/backend/convex/_generated/api'; export const handle: Handle = async ({ event, resolve }) => { event.locals.token = await getToken(createAuth, event.cookies); + // Enforcement para endpoints sensíveis (antes de chegar nas rotas) + // - Foco: /api/auth/* (login, logout, etc.) + // - Aplica blacklist + rate limit configuráveis via Convex + const pathname = event.url.pathname; + if (pathname.startsWith('/api/auth/')) { + const token = event.locals.token; + const client = createConvexHttpClient({ token: token || undefined }); + + // Preferir X-Forwarded-For quando existir (proxy), senão fallback do adapter + const forwardedFor = event.request.headers.get('x-forwarded-for'); + const ip = + forwardedFor?.split(',')[0]?.trim() || + event.request.headers.get('x-real-ip') || + event.getClientAddress(); + + try { + // 1) Enforcement básico (blacklist + rate limit) + const enforcement = await client.mutation(api.security.enforceRequest, { + ip, + path: pathname, + method: event.request.method + }); + + if (!enforcement.allowed) { + const headers = new Headers({ 'Content-Type': 'application/json' }); + if (enforcement.retryAfterMs) { + headers.set('Retry-After', String(Math.ceil(enforcement.retryAfterMs / 1000))); + } + return new Response(JSON.stringify(enforcement), { status: enforcement.status, headers }); + } + + // 2) Análise de ataques e bloqueio automático + // Extrair dados da requisição para análise + const headers: Record = {}; + event.request.headers.forEach((value, key) => { + headers[key] = value; + }); + + const queryParams: Record = {}; + event.url.searchParams.forEach((value, key) => { + queryParams[key] = value; + }); + + let body: string | undefined; + try { + // Tentar ler body apenas se for POST/PUT/PATCH + if (['POST', 'PUT', 'PATCH'].includes(event.request.method)) { + const clonedRequest = event.request.clone(); + body = await clonedRequest.text(); + } + } catch { + // Ignorar erros ao ler body + } + + const analise = await client.mutation(api.security.analisarRequisicaoHTTP, { + url: pathname + event.url.search, + method: event.request.method, + headers, + body, + queryParams, + ipOrigem: ip, + userAgent: event.request.headers.get('user-agent') ?? undefined + }); + + // Se ataque detectado e bloqueio automático aplicado, retornar 403 + if (analise.ataqueDetectado && analise.bloqueadoAutomatico) { + return new Response( + JSON.stringify({ + error: 'Acesso negado', + reason: 'ataque_detectado', + tipoAtaque: analise.tipoAtaque, + severidade: analise.severidade + }), + { + status: 403, + headers: { 'Content-Type': 'application/json' } + } + ); + } + } catch (err) { + // Se o enforcement falhar, não bloquear login (fail-open), + // mas registrar erro para observabilidade via handleError (se ocorrer) + console.error('❌ Falha no enforcement de segurança:', err); + } + } + return resolve(event); }; diff --git a/apps/web/src/lib/components/call/CallWindow.svelte b/apps/web/src/lib/components/call/CallWindow.svelte index b68873a..2e8f744 100644 --- a/apps/web/src/lib/components/call/CallWindow.svelte +++ b/apps/web/src/lib/components/call/CallWindow.svelte @@ -363,10 +363,51 @@ } try { - const config = configJitsi(); + const config = configJitsi; + if (!config) { + handleError( + 'Configuração Jitsi não encontrada', + 'Não foi possível obter a configuração do Jitsi. Verifique as configurações no painel de administração.' + ); + return; + } const { host, porta } = obterHostEPorta(config.domain); const protocol = config.useHttps ? 'https' : 'http'; + // Buscar token JWT se configurado + let jwtToken: string | null = null; + try { + const tokenResult = await client.action(api.configuracaoJitsi.gerarTokenJitsi, { + roomName, + conversaId, + chamadaId, + ambiente: config.ambiente || undefined + }); + + if (tokenResult.sucesso) { + jwtToken = tokenResult.token; + console.log('✅ Token JWT gerado com sucesso'); + } else { + console.warn('⚠️ Não foi possível gerar token JWT:', tokenResult.erro); + // Se JWT está configurado mas falhou, mostrar erro + if (tokenResult.erro?.includes('JWT Secret não configurado')) { + // JWT não está configurado, continuar sem token + console.log('ℹ️ JWT não configurado, continuando sem autenticação'); + } else { + // Outro erro (permissão, etc.) + handleError( + 'Erro ao gerar token de autenticação', + `Não foi possível gerar o token de autenticação: ${tokenResult.erro}. Verifique as configurações do Jitsi.` + ); + return; + } + } + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.warn('⚠️ Erro ao buscar token JWT (continuando sem token):', errorMessage); + // Continuar sem token se não for crítico + } + // Configuração conforme documentação oficial do Jitsi Meet // https://jitsi.github.io/handbook/docs/dev-guide/dev-guide-ljm-api/ const baseUrl = `${protocol}://${host}${porta && porta !== (config.useHttps ? 443 : 80) ? `:${porta}` : ''}`; @@ -415,10 +456,17 @@ baseUrl, serviceUrl: options.serviceUrl, muc: options.hosts?.muc, - focus: options.hosts?.focus + focus: options.hosts?.focus, + usandoJWT: jwtToken !== null }); - const connection = new JitsiMeetJS.JitsiConnection(null, null, options); + // Criar conexão com token JWT (se disponível) + // Segundo parâmetro é o token JWT, primeiro é appId (pode ser null se usar token) + const connection = new JitsiMeetJS.JitsiConnection( + jwtToken ? null : config.appId, // App ID (null se usar token) + jwtToken, // Token JWT + options + ); jitsiConnection = connection; setJitsiApi(connection); @@ -477,6 +525,54 @@ atualizarStatusConexao(false); const errorMsg = error instanceof Error ? error.message : String(error); + const errorStr = String(error).toLowerCase(); + + // Verificar se é erro relacionado a JWT + const isJWTError = + errorStr.includes('jwt') || + errorStr.includes('token') || + errorStr.includes('authentication') || + errorStr.includes('unauthorized') || + errorStr.includes('forbidden') || + errorMsg.includes('401') || + errorMsg.includes('403'); + + if (isJWTError) { + reconectando = false; + handleError( + 'Erro de autenticação JWT', + `Falha na autenticação com o servidor Jitsi. Isso pode ocorrer se:\n\n` + + `• O token JWT está expirado ou inválido\n` + + `• O JWT Secret está incorreto nas configurações\n` + + `• Você não tem permissão para acessar esta sala\n\n` + + `Verifique as configurações do Jitsi no painel de administração.\n\nErro: ${errorMsg}`, + false + ); + return; + } + + // Verificar se é erro de servidor inacessível + const isServerError = + errorStr.includes('network') || + errorStr.includes('timeout') || + errorStr.includes('connection refused') || + errorStr.includes('dns') || + errorMsg.includes('ECONNREFUSED') || + errorMsg.includes('ENOTFOUND'); + + if (isServerError) { + reconectando = false; + handleError( + 'Servidor Jitsi inacessível', + `Não foi possível conectar ao servidor Jitsi. Verifique:\n\n` + + `• Se o domínio está correto nas configurações\n` + + `• Se o servidor está online e acessível\n` + + `• Se há problemas de firewall ou rede\n\n` + + `Erro: ${errorMsg}`, + false + ); + return; + } // Tentar reconectar se ainda houver tentativas if (tentativasReconexao < MAX_TENTATIVAS_RECONEXAO) { diff --git a/apps/web/src/lib/components/chat/ChatWindow.svelte b/apps/web/src/lib/components/chat/ChatWindow.svelte index df1108f..1df41c9 100644 --- a/apps/web/src/lib/components/chat/ChatWindow.svelte +++ b/apps/web/src/lib/components/chat/ChatWindow.svelte @@ -171,6 +171,24 @@ return; } + // Verificar se Jitsi está configurado + try { + const configJitsi = await client.query(api.configuracaoJitsi.obterConfigJitsi, {}); + if (!configJitsi || !configJitsi.ativo) { + errorTitle = 'Jitsi não configurado'; + errorMessage = + 'O sistema de videochamadas não está configurado. Entre em contato com o administrador do sistema para configurar o Jitsi.'; + errorInstructions = + 'Um administrador precisa configurar o servidor Jitsi no painel de administração antes que as chamadas possam ser iniciadas.'; + errorDetails = undefined; + showErrorModal = true; + return; + } + } catch (error: unknown) { + console.error('Erro ao verificar configuração Jitsi:', error); + // Continuar mesmo se houver erro na verificação (pode ser problema temporário) + } + try { iniciandoChamada = true; const chamadaId = await client.mutation(api.chamadas.criarChamada, { diff --git a/apps/web/src/lib/components/ti/AttackCapabilitiesSection.svelte b/apps/web/src/lib/components/ti/AttackCapabilitiesSection.svelte new file mode 100644 index 0000000..c64f4d3 --- /dev/null +++ b/apps/web/src/lib/components/ti/AttackCapabilitiesSection.svelte @@ -0,0 +1,152 @@ + + +
+ +
+
+
+ +
+
Total de Tipos
+
{stats.total}
+
Tipos de ataques monitorados
+
+ +
+
+ +
+
Com Bloqueio Automático
+
{stats.withAutoBlock}
+
Tipos com mitigação ativa
+
+ +
+
+ +
+
Taxa de Cobertura
+
+ {Math.round((stats.withAutoBlock / stats.total) * 100)}% +
+
Cobertura de mitigação ativa
+
+
+ + +
+

Capacidades por Grupo de Ataque

+ + {#each grupos as grupo} + {@const capabilities = getCapabilitiesByGroup(grupo.id as any)} +
+
+

{grupo.nome}

+

+ {capabilities.length} tipo{capabilities.length !== 1 ? 's' : ''} de ataque +

+ +
+ {#each capabilities as cap} +
abrirModal(cap.tipo)} + > +
+
+
{cap.nome}
+ +
+ +
+ + {severidadeLabels[cap.severidadePadrao]} + + {#if cap.deteccao.tempoReal} + Tempo Real + {/if} +
+ +
+ {#if cap.acoes.bloqueioAutomatico} + 🔒 + {/if} + {#if cap.acoes.blacklist} + 📋 + {/if} + {#if cap.acoes.rateLimit} + + {/if} + {#if cap.acoes.alerta} + 📢 + {/if} +
+ +

+ {cap.descricao} +

+
+
+ {/each} +
+
+
+ {/each} +
+
+ + diff --git a/apps/web/src/lib/components/ti/AttackDetailsModal.svelte b/apps/web/src/lib/components/ti/AttackDetailsModal.svelte new file mode 100644 index 0000000..bd315a3 --- /dev/null +++ b/apps/web/src/lib/components/ti/AttackDetailsModal.svelte @@ -0,0 +1,187 @@ + + +{#if aberto && capability} + +{/if} diff --git a/apps/web/src/lib/components/ti/AttackTypeTooltip.svelte b/apps/web/src/lib/components/ti/AttackTypeTooltip.svelte new file mode 100644 index 0000000..e32462b --- /dev/null +++ b/apps/web/src/lib/components/ti/AttackTypeTooltip.svelte @@ -0,0 +1,80 @@ + + +
+ +
+ + diff --git a/apps/web/src/lib/components/ti/CybersecurityWizcard.svelte b/apps/web/src/lib/components/ti/CybersecurityWizcard.svelte index 96b8871..beec942 100644 --- a/apps/web/src/lib/components/ti/CybersecurityWizcard.svelte +++ b/apps/web/src/lib/components/ti/CybersecurityWizcard.svelte @@ -10,6 +10,10 @@ import autoTable from 'jspdf-autotable'; import { browser } from '$app/environment'; import { authStore } from '$lib/stores/auth.svelte'; + import AttackDetailsModal from './AttackDetailsModal.svelte'; + import AttackCapabilitiesSection from './AttackCapabilitiesSection.svelte'; + import { attackCapabilities } from '$lib/data/attackCapabilities'; + import { Info } from 'lucide-svelte'; const client = useConvexClient(); const visaoCamadas = useQuery(api.security.obterVisaoCamadas, { @@ -136,8 +140,7 @@ severidadeMin: alertSeveridadeMin, tiposAtaque: alertTiposAtaque as AtaqueCiberneticoTipo[], reenvioMin: alertReenvioMin, - templateCodigo: alertTemplate, // Incluir template selecionado - criadoPor: obterUsuarioId() + templateCodigo: alertTemplate // Incluir template selecionado }); feedback = { tipo: 'success', @@ -226,7 +229,6 @@ xxe: 'XXE', man_in_the_middle: 'MITM', ddos: 'DDoS', - engenharia_social: 'Engenharia Social', cve_exploit: 'Exploração de CVE', apt: 'APT', zero_day: 'Zero-Day', @@ -276,6 +278,13 @@ let feedback = $state<{ tipo: 'success' | 'error'; mensagem: string } | null>(null); let ultimaReferenciaCritica: Id<'securityEvents'> | null = null; let audioCtx: AudioContext | null = null; + let confirmDialog: HTMLDialogElement | null = null; + let confirmTitulo = $state('Confirmar ação'); + let confirmMensagem = $state(''); + let confirmConfirmarLabel = $state('Confirmar'); + let confirmCancelLabel = $state('Cancelar'); + let confirmBusy = $state(false); + let confirmAcao = $state Promise)>(null); // Rate Limiting let mostrarRateLimitConfig = $state(false); @@ -293,6 +302,113 @@ let rateLimitNotas = $state(''); let rateLimitEditando = $state | null>(null); + // Bloqueio Automático + let mostrarAutoBlockConfig = $state(false); + let autoBlockTipoAtaque = $state('sql_injection'); + let autoBlockBloquearAutomatico = $state(true); + let autoBlockSeveridadeMinima = $state('alto'); + let autoBlockDuracaoSegundos = $state(undefined); + let autoBlockAtivo = $state(true); + let autoBlockEditando = $state | null>(null); + + async function salvarAutoBlockConfig() { + try { + if (autoBlockEditando) { + await client.mutation(api.security.atualizarAutoBlockConfig, { + configId: autoBlockEditando, + bloquearAutomatico: autoBlockBloquearAutomatico, + severidadeMinima: autoBlockSeveridadeMinima, + duracaoBloqueioSegundos: autoBlockDuracaoSegundos, + ativo: autoBlockAtivo + }); + feedback = { tipo: 'success', mensagem: 'Configuração atualizada com sucesso.' }; + } else { + await client.mutation(api.security.criarAutoBlockConfig, { + tipoAtaque: autoBlockTipoAtaque, + bloquearAutomatico: autoBlockBloquearAutomatico, + severidadeMinima: autoBlockSeveridadeMinima, + duracaoBloqueioSegundos: autoBlockDuracaoSegundos, + ativo: autoBlockAtivo + }); + feedback = { tipo: 'success', mensagem: 'Configuração criada com sucesso.' }; + } + resetarFormAutoBlock(); + mostrarAutoBlockConfig = false; + } catch (erro: unknown) { + feedback = { tipo: 'error', mensagem: mensagemErro(erro) }; + } + } + + function resetarFormAutoBlock() { + autoBlockTipoAtaque = 'sql_injection'; + autoBlockBloquearAutomatico = true; + autoBlockSeveridadeMinima = 'alto'; + autoBlockDuracaoSegundos = undefined; + autoBlockAtivo = true; + autoBlockEditando = null; + } + + async function deletarAutoBlockConfig(configId: Id<'autoBlockConfig'>) { + confirmTitulo = 'Confirmar exclusão'; + confirmMensagem = 'Tem certeza que deseja excluir esta configuração de bloqueio automático?'; + confirmConfirmarLabel = 'Excluir'; + confirmCancelLabel = 'Cancelar'; + confirmAcao = async () => { + try { + await client.mutation(api.security.deletarAutoBlockConfig, { configId }); + feedback = { tipo: 'success', mensagem: 'Configuração excluída com sucesso.' }; + } catch (erro: unknown) { + feedback = { tipo: 'error', mensagem: mensagemErro(erro) }; + } + }; + confirmDialog?.showModal(); + } + + function editarAutoBlockConfig(config: { + _id: Id<'autoBlockConfig'>; + tipoAtaque: AtaqueCiberneticoTipo; + bloquearAutomatico: boolean; + severidadeMinima: SeveridadeSeguranca; + duracaoBloqueioSegundos?: number; + ativo: boolean; + }) { + autoBlockTipoAtaque = config.tipoAtaque; + autoBlockBloquearAutomatico = config.bloquearAutomatico; + autoBlockSeveridadeMinima = config.severidadeMinima; + autoBlockDuracaoSegundos = config.duracaoBloqueioSegundos; + autoBlockAtivo = config.ativo; + autoBlockEditando = config._id; + mostrarAutoBlockConfig = true; + } + + // Capacidades e detalhes de ataques + let mostrarCapacidades = $state(false); + let modalAtaqueAberto = $state(false); + let tipoAtaqueSelecionado = $state(null); + const autoBlockConfigs = useQuery(api.security.listarAutoBlockConfigs, { + ativo: true, + limit: 100 + }); + + function abrirModalAtaque(tipo: AtaqueCiberneticoTipo) { + tipoAtaqueSelecionado = tipo; + modalAtaqueAberto = true; + } + + function fecharModalAtaque() { + modalAtaqueAberto = false; + tipoAtaqueSelecionado = null; + } + + // Verificar se um tipo de ataque tem bloqueio automático ativo + function temBloqueioAutomatico(tipo: AtaqueCiberneticoTipo): boolean { + return ( + autoBlockConfigs?.data?.some( + (config) => config.tipoAtaque === tipo && config.bloquearAutomatico && config.ativo + ) ?? false + ); + } + let series = $derived.by(() => visaoCamadas?.data?.series ?? []); let totais = $derived.by(() => visaoCamadas?.data?.totais ?? null); let eventosFiltrados = $derived.by(() => { @@ -453,9 +569,33 @@ if (!browser) { return; } + tocarAlertaSonoro().catch(() => { + // Ignorar falhas de autoplay policy + }); + }); + + async function prepararAudioContext() { + if (!browser) return; + try { + if (!audioCtx) { + audioCtx = new AudioContext(); + } + if (audioCtx.state === 'suspended') { + await audioCtx.resume(); + } + } catch (erro) { + console.warn('Falha ao preparar áudio:', erro); + } + } + + async function tocarAlertaSonoro() { + if (!browser) return; if (!audioCtx) { audioCtx = new AudioContext(); } + if (audioCtx.state === 'suspended') { + await audioCtx.resume(); + } const oscillator = audioCtx.createOscillator(); const gain = audioCtx.createGain(); oscillator.type = 'triangle'; @@ -463,10 +603,42 @@ gain.gain.value = 0.08; oscillator.connect(gain).connect(audioCtx.destination); oscillator.start(); - setTimeout(() => { - oscillator.stop(); - }, 300); - }); + setTimeout(() => oscillator.stop(), 300); + } + + function abrirConfirmacao(params: { + titulo: string; + mensagem: string; + confirmarLabel?: string; + cancelarLabel?: string; + acao: () => Promise; + }) { + confirmTitulo = params.titulo; + confirmMensagem = params.mensagem; + confirmConfirmarLabel = params.confirmarLabel ?? 'Confirmar'; + confirmCancelLabel = params.cancelarLabel ?? 'Cancelar'; + confirmAcao = params.acao; + confirmBusy = false; + confirmDialog?.showModal(); + } + + async function confirmarAcao() { + if (!confirmAcao) return; + try { + confirmBusy = true; + await confirmAcao(); + confirmDialog?.close(); + } finally { + confirmBusy = false; + confirmAcao = null; + } + } + + function cancelarConfirmacao() { + if (confirmBusy) return; + confirmDialog?.close(); + confirmAcao = null; + } function mensagemErro(erro: unknown): string { if (erro instanceof Error) return erro.message; @@ -498,7 +670,6 @@ } try { await client.mutation(api.security.atualizarReputacaoIndicador, { - usuarioId: obterUsuarioId(), indicador: ipAlvo, categoria: 'ip', acao: acaoSelecionada, @@ -512,7 +683,6 @@ eventoId, tipo: acaoSelecionada === 'forcar_blacklist' ? 'block_ip' : 'unblock_ip', origem: 'manual', - executadoPor: obterUsuarioId(), detalhes: comentarioManual || 'Ação executada via Wizcard', resultado: 'registrado', relacionadoA: undefined @@ -537,7 +707,6 @@ e.preventDefault(); try { await client.mutation(api.security.configurarRegraPorta, { - usuarioId: obterUsuarioId(), porta, protocolo, acao, @@ -944,33 +1113,38 @@ doc.save(nomeArquivo); } catch (error) { console.error('Erro ao gerar PDF:', error); - alert('Erro ao gerar PDF do relatório. Tente novamente.'); + feedback = { tipo: 'error', mensagem: 'Erro ao gerar PDF do relatório. Tente novamente.' }; } } async function excluirRelatorio(relatorioId: Id<'reportRequests'>) { - if (!confirm('Excluir este relatório? Esta ação não pode ser desfeita.')) return; - try { - const resp = await client.mutation(api.security.deletarRelatorio, { - relatorioId - }); - if (resp?.success) { - feedback = { tipo: 'success', mensagem: 'Relatório excluído.' }; - } else { - feedback = { - tipo: 'error', - mensagem: 'Não foi possível excluir o relatório.' - }; + abrirConfirmacao({ + titulo: 'Excluir relatório?', + mensagem: 'Esta ação não pode ser desfeita.', + confirmarLabel: 'Excluir', + acao: async () => { + try { + const resp = await client.mutation(api.security.deletarRelatorio, { + relatorioId + }); + if (resp?.success) { + feedback = { tipo: 'success', mensagem: 'Relatório excluído.' }; + } else { + feedback = { + tipo: 'error', + mensagem: 'Não foi possível excluir o relatório.' + }; + } + } catch (erro: unknown) { + feedback = { tipo: 'error', mensagem: mensagemErro(erro) }; + } } - } catch (erro: unknown) { - feedback = { tipo: 'error', mensagem: mensagemErro(erro) }; - } + }); } async function gerarRelatorioAvancado(e: SubmitEvent) { e.preventDefault(); try { await client.mutation(api.security.solicitarRelatorioSeguranca, { - solicitanteId: obterUsuarioId(), filtros: { dataInicio: parseDatetime(relatorioInicio), dataFim: parseDatetime(relatorioFim), @@ -1028,7 +1202,6 @@ if (rateLimitEditando) { await client.mutation(api.security.atualizarConfigRateLimit, { configId: rateLimitEditando, - usuarioId: obterUsuarioId(), nome: rateLimitNome, limite: rateLimitLimite, janelaSegundos: rateLimitJanelaSegundos, @@ -1044,7 +1217,6 @@ }; } else { await client.mutation(api.security.criarConfigRateLimit, { - usuarioId: obterUsuarioId(), nome: rateLimitNome, tipo: rateLimitTipo, identificador: @@ -1070,26 +1242,30 @@ } async function deletarConfigRateLimit(configId: Id<'rateLimitConfig'>) { - if (!confirm('Tem certeza que deseja deletar esta configuração de rate limit?')) return; - try { - await client.mutation(api.security.deletarConfigRateLimit, { - configId, - usuarioId: obterUsuarioId() - }); - feedback = { - tipo: 'success', - mensagem: 'Configuração de rate limit deletada com sucesso.' - }; - } catch (erro: unknown) { - feedback = { tipo: 'error', mensagem: mensagemErro(erro) }; - } + abrirConfirmacao({ + titulo: 'Deletar configuração de rate limit?', + mensagem: 'Tem certeza que deseja deletar esta configuração?', + confirmarLabel: 'Deletar', + acao: async () => { + try { + await client.mutation(api.security.deletarConfigRateLimit, { + configId + }); + feedback = { + tipo: 'success', + mensagem: 'Configuração de rate limit deletada com sucesso.' + }; + } catch (erro: unknown) { + feedback = { tipo: 'error', mensagem: mensagemErro(erro) }; + } + } + }); } async function toggleAtivoRateLimit(configId: Id<'rateLimitConfig'>, ativo: boolean) { try { await client.mutation(api.security.atualizarConfigRateLimit, { configId, - usuarioId: obterUsuarioId(), ativo: !ativo }); feedback = { @@ -1115,6 +1291,29 @@
+ + + + + {#if feedback}
@@ -1169,7 +1368,14 @@
@@ -1506,10 +1723,33 @@ {severityLabels[evento.severidade]} - {attackLabels[evento.tipoAtaque]} +
+ {attackLabels[evento.tipoAtaque]} + + {#if temBloqueioAutomatico(evento.tipoAtaque)} + + 🔒 + + {/if} +
{evento.status} + {#if evento.tags?.includes('bloqueio_automatico')} + + 🔒 Auto-Bloqueado + + {/if}
{formatarData(evento.timestamp)}
@@ -1553,7 +1793,6 @@ onclick={async () => { try { await client.mutation(api.security.solicitarRelatorioSeguranca, { - solicitanteId: obterUsuarioId(), filtros: { dataInicio: evento.timestamp - 15 * 60 * 1000, dataFim: evento.timestamp + 15 * 60 * 1000, @@ -1647,7 +1886,6 @@ onclick={async () => { try { await client.mutation(api.security.configurarRegraPorta, { - usuarioId: obterUsuarioId(), regraId: regra._id, porta: regra.porta, protocolo: regra.protocolo, @@ -1666,18 +1904,22 @@ @@ -2052,6 +2294,200 @@
+ +
+
+
+

Bloqueio Automático de Ataques

+

+ Configure bloqueio automático por tipo de ataque. Quando um ataque é detectado e atende aos + critérios, o IP será bloqueado automaticamente. +

+
+ +
+ + {#if mostrarAutoBlockConfig} +
{ e.preventDefault(); salvarAutoBlockConfig(); }} + > +
+ + +
+ + + + {#if autoBlockBloquearAutomatico} + + {/if} + + + +
+ + {#if autoBlockEditando} + + {/if} +
+
+ {/if} + +
+

Configurações Ativas

+ {#if autoBlockConfigs?.data && autoBlockConfigs.data.length > 0} +
+ + + + + + + + + + + + + {#each autoBlockConfigs.data as config (config._id)} + + + + + + + + + {/each} + +
Tipo de AtaqueSeveridade MínimaBloqueio AutoDuraçãoStatusAções
+
+ {attackLabels[config.tipoAtaque]} + +
+
+ {severityLabels[config.severidadeMinima]} + + {#if config.bloquearAutomatico} + Sim + {:else} + Não + {/if} + + {#if config.duracaoBloqueioSegundos} + {Math.floor(config.duracaoBloqueioSegundos / 60)} min + {:else} + Permanente + {/if} + + {#if config.ativo} + Ativo + {:else} + Inativo + {/if} + +
+ + +
+
+
+ {:else} +
+

Nenhuma configuração de bloqueio automático.

+

+ Configure bloqueio automático para tipos de ataques acima +

+
+ {/if} +
+
+
{/if}
+ + + {#if mostrarCapacidades} +
+
+

Capacidades de Detecção e Mitigação

+ +
+ +
+ {/if}
+ + +