From 8e75018a35b3719f040252ae17bc90b6a963a096 Mon Sep 17 00:00:00 2001 From: "Rodrigo B. de Sousa Martins" Date: Mon, 28 Jul 2025 15:51:41 -0300 Subject: [PATCH] add webproof system Signed-off-by: Rodrigo B. de Sousa Martins --- prisma/schema-navegacao.prisma | 106 +++++++ src/App.tsx | 6 + src/components/navegacao/IniciarNavegacao.tsx | 119 ++++++++ src/server/index.ts | 4 +- src/server/routes/navegacao.ts | 168 +++++++++++ src/services/NavegacaoService.ts | 264 ++++++++++++++++++ 6 files changed, 666 insertions(+), 1 deletion(-) create mode 100644 prisma/schema-navegacao.prisma create mode 100644 src/components/navegacao/IniciarNavegacao.tsx create mode 100644 src/server/routes/navegacao.ts create mode 100644 src/services/NavegacaoService.ts diff --git a/prisma/schema-navegacao.prisma b/prisma/schema-navegacao.prisma new file mode 100644 index 0000000..3c623a3 --- /dev/null +++ b/prisma/schema-navegacao.prisma @@ -0,0 +1,106 @@ +// Adicionar ao schema.prisma existente (não substitui) +model SessaoNavegacao { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + titulo String + descricao String? + status StatusSessao @default(INICIADA) + usuarioId String @db.Uuid + usuario Usuario @relation(fields: [usuarioId], references: [id]) + + // Dados da sessão + sessionKey String @unique + browserFingerprint Json + inicioSessao DateTime @default(now()) + fimSessao DateTime? + duracaoSegundos Int? + + // Arquivos gerados + videoPath String? + screenshotsPaths String[] + metadataPath String? + relatorioPdfPath String? + + // Hashes e validação + checksumVideo String? + checksumMetadata String? + timestampProof String? + + // Navegação registrada + acoes AcaoNavegacao[] + capturas CapturaNavegacao[] + + criadoEm DateTime @default(now()) + atualizadoEm DateTime @updatedAt + + @@map("sessoes_navegacao") +} + +model AcaoNavegacao { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + sessaoId String @db.Uuid + sessao SessaoNavegacao @relation(fields: [sessaoId], references: [id], onDelete: Cascade) + + timestamp DateTime + tipo TipoAcao + url String? + elemento String? + coordenadas Json? + valor String? + screenshot String? + + metadados Json + + criadoEm DateTime @default(now()) + + @@map("acoes_navegacao") +} + +model CapturaNavegacao { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + sessaoId String @db.Uuid + sessao SessaoNavegacao @relation(fields: [sessaoId], references: [id], onDelete: Cascade) + + timestamp DateTime + url String + titulo String? + screenshotPath String + htmlContent String? + checksumHtml String? + checksumScreenshot String + + // Metadados da página + networkRequests Json[] + cookies Json[] + localStorage Json? + sessionStorage Json? + + criadoEm DateTime @default(now()) + + @@map("capturas_navegacao") +} + +enum StatusSessao { + INICIADA + EM_ANDAMENTO + FINALIZANDO + CONCLUIDA + ERRO + CANCELADA +} + +enum TipoAcao { + NAVEGACAO + CLIQUE + DIGITACAO + SCROLL + HOVER + DOWNLOAD + FORMULARIO + POPUP + REDIRECT +} + +// Adicionar relação ao modelo Usuario existente +// Usuario { +// sessoesNavegacao SessaoNavegacao[] +// } diff --git a/src/App.tsx b/src/App.tsx index 3a7a7c7..f7f57e7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -49,6 +49,7 @@ import { NovoCalculo } from './components/calculos/NovoCalculo'; import { DetalhesCalculo } from './components/calculos/DetalhesCalculo'; import { NovoPensaoAlimenticia } from './components/calculos/NovoPensaoAlimenticia'; import { DetalhePensaoAlimenticia } from './components/calculos/DetalhePensaoAlimenticia'; +import { IniciarNavegacao } from './components/navegacao/IniciarNavegacao'; const queryClient = new QueryClient(); @@ -126,6 +127,11 @@ const router = createBrowserRouter( } /> + + + + } /> } /> diff --git a/src/components/navegacao/IniciarNavegacao.tsx b/src/components/navegacao/IniciarNavegacao.tsx new file mode 100644 index 0000000..d0c3380 --- /dev/null +++ b/src/components/navegacao/IniciarNavegacao.tsx @@ -0,0 +1,119 @@ +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { toast } from 'react-toastify'; +import { + Container, + Card, + CardContent, + Typography, + TextField, + Button, + Box, + Alert, + CircularProgress +} from '@mui/material'; + +export const IniciarNavegacao: React.FC = () => { + const [titulo, setTitulo] = useState(''); + const [descricao, setDescricao] = useState(''); + const [iniciando, setIniciando] = useState(false); + const navigate = useNavigate(); + + const handleIniciar = async () => { + if (!titulo.trim()) { + toast.error('Título é obrigatório'); + return; + } + + try { + setIniciando(true); + + const response = await fetch('/api/navegacao/iniciar', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${localStorage.getItem('token')}` + }, + body: JSON.stringify({ titulo, descricao }) + }); + + if (!response.ok) { + throw new Error('Erro ao iniciar sessão'); + } + + const resultado = await response.json(); + + toast.success('Sessão de navegação iniciada!'); + navigate(`/navegacao/sessao/${resultado.sessaoId}`); + + } catch (error) { + console.error('Erro:', error); + toast.error('Erro ao iniciar sessão de navegação'); + } finally { + setIniciando(false); + } + }; + + return ( + + + + + + Nova Sessão de Coleta de Provas + + + + + Este sistema criará uma sessão controlada de navegação onde todas as ações + serão gravadas e registradas para fins de coleta de provas digitais. + + + + + setTitulo(e.target.value)} + margin="normal" + required + helperText="Descreva brevemente o objetivo desta coleta" + /> + + setDescricao(e.target.value)} + margin="normal" + multiline + rows={3} + helperText="Informações adicionais sobre o que será coletado" + /> + + + + + + + + + + + + ); +}; diff --git a/src/server/index.ts b/src/server/index.ts index 96bf2e4..71d84a6 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -27,6 +27,7 @@ import indicesMonetariosRoutes from './routes/indicesMonetarios'; import pensaoAlimenticiaRouter from './routes/pensaoAlimenticia'; // Importar router de pensão alimentícia import financeiroPainelRoutes from './routes/financeiroPainel'; // Importar rotas do financeiro painel import audienciasRoutes from './routes/audiencias'; // Importar rotas de audiências +import navegacaoRoutes from './routes/navegacao'; // Adicionar esta importação const app = express(); @@ -1481,4 +1482,5 @@ app.use('/api/calculos', calculosJudiciaisRoutes); app.use('/api/indices', indicesMonetariosRoutes); app.use('/api/pensao-alimenticia', pensaoAlimenticiaRouter); app.use('/api/financeiro-painel', financeiroPainelRoutes); // Adicionar uso das rotas do financeiro painel -app.use('/api/audiencias', audienciasRoutes); // Adicionar uso das rotas de audiências \ No newline at end of file +app.use('/api/audiencias', audienciasRoutes); // Adicionar uso das rotas de audiências +app.use('/api/navegacao', navegacaoRoutes); // Adicionar esta linha junto com as outras rotas \ No newline at end of file diff --git a/src/server/routes/navegacao.ts b/src/server/routes/navegacao.ts new file mode 100644 index 0000000..a155b0f --- /dev/null +++ b/src/server/routes/navegacao.ts @@ -0,0 +1,168 @@ +import { Router } from 'express'; +import { NavegacaoService } from '../services/NavegacaoService'; +import { authMiddleware } from '../../middleware/auth'; +import { prisma } from '../prismaClient'; + +const router = Router(); + +// Aplicar middleware de autenticação +router.use(authMiddleware); + +// Iniciar nova sessão de navegação +router.post('/iniciar', async (req, res) => { + try { + const { titulo, descricao } = req.body; + + if (!req.user?.id) { + return res.status(401).json({ error: 'Usuário não autenticado' }); + } + + const navegacaoService = new NavegacaoService(); + const resultado = await navegacaoService.iniciarSessao( + req.user.id, + titulo, + descricao + ); + + res.json(resultado); + } catch (error) { + console.error('Erro ao iniciar navegação:', error); + res.status(500).json({ + error: 'Erro ao iniciar sessão de navegação', + details: error.message + }); + } +}); + +// Navegar para URL específica +router.post('/:sessaoId/navegar', async (req, res) => { + try { + const { sessaoId } = req.params; + const { url } = req.body; + + // Verificar se a sessão pertence ao usuário + const sessao = await prisma.sessaoNavegacao.findFirst({ + where: { + id: sessaoId, + usuarioId: req.user!.id + } + }); + + if (!sessao) { + return res.status(404).json({ error: 'Sessão não encontrada' }); + } + + const navegacaoService = new NavegacaoService(); + await navegacaoService.navegarPara(url); + + res.json({ success: true }); + } catch (error) { + console.error('Erro na navegação:', error); + res.status(500).json({ error: 'Erro ao navegar' }); + } +}); + +// Finalizar sessão +router.post('/:sessaoId/finalizar', async (req, res) => { + try { + const { sessaoId } = req.params; + + const sessao = await prisma.sessaoNavegacao.findFirst({ + where: { + id: sessaoId, + usuarioId: req.user!.id + } + }); + + if (!sessao) { + return res.status(404).json({ error: 'Sessão não encontrada' }); + } + + const navegacaoService = new NavegacaoService(); + await navegacaoService.finalizarSessao(); + + res.json({ success: true }); + } catch (error) { + console.error('Erro ao finalizar sessão:', error); + res.status(500).json({ error: 'Erro ao finalizar sessão' }); + } +}); + +// Listar sessões do usuário +router.get('/sessoes', async (req, res) => { + try { + const sessoes = await prisma.sessaoNavegacao.findMany({ + where: { usuarioId: req.user!.id }, + include: { + acoes: { + select: { id: true, timestamp: true, tipo: true } + }, + capturas: { + select: { id: true, timestamp: true, url: true } + } + }, + orderBy: { criadoEm: 'desc' } + }); + + res.json(sessoes); + } catch (error) { + console.error('Erro ao listar sessões:', error); + res.status(500).json({ error: 'Erro ao listar sessões' }); + } +}); + +// Detalhes de uma sessão +router.get('/sessoes/:sessaoId', async (req, res) => { + try { + const { sessaoId } = req.params; + + const sessao = await prisma.sessaoNavegacao.findFirst({ + where: { + id: sessaoId, + usuarioId: req.user!.id + }, + include: { + acoes: { + orderBy: { timestamp: 'asc' } + }, + capturas: { + orderBy: { timestamp: 'asc' } + } + } + }); + + if (!sessao) { + return res.status(404).json({ error: 'Sessão não encontrada' }); + } + + res.json(sessao); + } catch (error) { + console.error('Erro ao buscar sessão:', error); + res.status(500).json({ error: 'Erro ao buscar detalhes da sessão' }); + } +}); + +// Download do relatório +router.get('/sessoes/:sessaoId/relatorio', async (req, res) => { + try { + const { sessaoId } = req.params; + + const sessao = await prisma.sessaoNavegacao.findFirst({ + where: { + id: sessaoId, + usuarioId: req.user!.id + } + }); + + if (!sessao || !sessao.relatorioPdfPath) { + return res.status(404).json({ error: 'Relatório não encontrado' }); + } + + res.download(sessao.relatorioPdfPath); + } catch (error) { + console.error('Erro ao baixar relatório:', error); + res.status(500).json({ error: 'Erro ao baixar relatório' }); + } +}); + +export default router; diff --git a/src/services/NavegacaoService.ts b/src/services/NavegacaoService.ts new file mode 100644 index 0000000..938636b --- /dev/null +++ b/src/services/NavegacaoService.ts @@ -0,0 +1,264 @@ +import { prisma } from '../config/database'; +import puppeteer from 'puppeteer-extra'; +import StealthPlugin from 'puppeteer-extra-plugin-stealth'; +import { createHash } from 'crypto'; +import * as path from 'path'; +import * as fs from 'fs/promises'; +import { Browser, Page } from 'puppeteer'; +import * as OpenTimestamps from 'opentimestamps'; + +puppeteer.use(StealthPlugin()); + +export class NavegacaoService { + private browser: Browser | null = null; + private page: Page | null = null; + private sessaoId: string | null = null; + private recording: boolean = false; + private recordingInterval: NodeJS.Timeout | null = null; + + async iniciarSessao(usuarioId: string, titulo: string, descricao?: string) { + try { + // Criar sessão no banco + const sessionKey = this.generateSessionKey(); + + const sessao = await prisma.sessaoNavegacao.create({ + data: { + titulo, + descricao, + usuarioId, + sessionKey, + status: 'INICIADA', + browserFingerprint: await this.getBrowserFingerprint() + } + }); + + this.sessaoId = sessao.id; + + // Inicializar browser controlado + await this.setupBrowser(); + + return { + sessaoId: sessao.id, + sessionKey: sessao.sessionKey, + browserWsEndpoint: this.browser?.wsEndpoint() + }; + } catch (error) { + console.error('Erro ao iniciar sessão:', error); + throw new Error('Falha ao iniciar sessão de navegação'); + } + } + + private async setupBrowser() { + this.browser = await puppeteer.launch({ + headless: false, + devtools: true, + defaultViewport: null, + args: [ + '--no-sandbox', + '--disable-setuid-sandbox', + '--enable-webrtc-logs', + '--use-fake-ui-for-media-stream', + '--autoplay-policy=no-user-gesture-required' + ] + }); + + this.page = await this.browser.newPage(); + + // Configurar interceptadores + await this.setupPageInterceptors(); + } + + private async setupPageInterceptors() { + if (!this.page) return; + + // Interceptar requests de rede + await this.page.setRequestInterception(true); + + this.page.on('request', async (request) => { + await this.registrarAcao('NAVEGACAO', { + url: request.url(), + method: request.method(), + headers: request.headers(), + postData: request.postData() + }); + request.continue(); + }); + + // Interceptar cliques + await this.page.evaluateOnNewDocument(() => { + document.addEventListener('click', (event) => { + window.postMessage({ + type: 'NAVEGACAO_ACTION', + action: 'CLIQUE', + data: { + x: event.clientX, + y: event.clientY, + target: event.target?.tagName, + targetId: event.target?.id, + targetClass: event.target?.className + } + }, '*'); + }); + }); + + // Escutar mensagens do frontend + this.page.on('console', async (msg) => { + if (msg.text().includes('NAVEGACAO_ACTION')) { + const data = JSON.parse(msg.text().replace('NAVEGACAO_ACTION:', '')); + await this.registrarAcao(data.action, data.data); + } + }); + } + + async navegarPara(url: string) { + if (!this.page || !this.sessaoId) { + throw new Error('Sessão não iniciada'); + } + + try { + await this.page.goto(url, { waitUntil: 'networkidle2' }); + + // Capturar estado atual + await this.capturarEstadoAtual(); + + // Atualizar status + await prisma.sessaoNavegacao.update({ + where: { id: this.sessaoId }, + data: { status: 'EM_ANDAMENTO' } + }); + + } catch (error) { + console.error('Erro na navegação:', error); + throw error; + } + } + + private async capturarEstadoAtual() { + if (!this.page || !this.sessaoId) return; + + const url = this.page.url(); + const titulo = await this.page.title(); + const timestamp = new Date(); + + // Screenshot + const screenshotBuffer = await this.page.screenshot({ fullPage: true }); + const screenshotPath = await this.salvarArquivo( + screenshotBuffer, + `screenshot_${Date.now()}.png` + ); + + // HTML content + const htmlContent = await this.page.content(); + const checksumHtml = createHash('sha256').update(htmlContent).digest('hex'); + const checksumScreenshot = createHash('sha256').update(screenshotBuffer).digest('hex'); + + // Cookies e storage + const cookies = await this.page.cookies(); + const localStorage = await this.page.evaluate(() => + JSON.stringify(window.localStorage) + ); + const sessionStorage = await this.page.evaluate(() => + JSON.stringify(window.sessionStorage) + ); + + // Salvar captura + await prisma.capturaNavegacao.create({ + data: { + sessaoId: this.sessaoId, + timestamp, + url, + titulo, + screenshotPath, + htmlContent, + checksumHtml, + checksumScreenshot, + networkRequests: [], // Será preenchido pelos interceptadores + cookies, + localStorage: localStorage ? JSON.parse(localStorage) : null, + sessionStorage: sessionStorage ? JSON.parse(sessionStorage) : null + } + }); + } + + private async registrarAcao(tipo: string, dados: any) { + if (!this.sessaoId) return; + + const screenshot = this.page ? await this.page.screenshot({ encoding: 'base64' }) : null; + + await prisma.acaoNavegacao.create({ + data: { + sessaoId: this.sessaoId, + timestamp: new Date(), + tipo: tipo as any, + url: this.page?.url(), + elemento: dados.target, + coordenadas: dados.x && dados.y ? { x: dados.x, y: dados.y } : null, + valor: dados.value, + screenshot: screenshot ? `data:image/png;base64,${screenshot}` : null, + metadados: dados + } + }); + } + + async finalizarSessao() { + if (!this.sessaoId) return; + + try { + await prisma.sessaoNavegacao.update({ + where: { id: this.sessaoId }, + data: { + status: 'FINALIZANDO', + fimSessao: new Date() + } + }); + + // Gerar relatório final + await this.gerarRelatorioFinal(); + + if (this.browser) { + await this.browser.close(); + } + + await prisma.sessaoNavegacao.update({ + where: { id: this.sessaoId }, + data: { status: 'CONCLUIDA' } + }); + + } catch (error) { + console.error('Erro ao finalizar sessão:', error); + await prisma.sessaoNavegacao.update({ + where: { id: this.sessaoId }, + data: { status: 'ERRO' } + }); + } + } + + private async gerarRelatorioFinal() { + // Implementar geração do relatório PDF com todas as evidências + // Compilar screenshots, metadados, timestamps, etc. + } + + private generateSessionKey(): string { + return createHash('sha256') + .update(`${Date.now()}_${Math.random()}_${process.pid}`) + .digest('hex'); + } + + private async getBrowserFingerprint() { + return { + userAgent: 'Controlled Browser', + timestamp: new Date().toISOString(), + nodeVersion: process.version + }; + } + + private async salvarArquivo(buffer: Buffer, filename: string): Promise { + const dir = path.join(process.cwd(), 'storage', 'navegacao', this.sessaoId!); + await fs.mkdir(dir, { recursive: true }); + + const filepath = path.join(dir, filename); + await fs.writeFile(filepath, buffer); + + return filepath; + } +}