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"
+ />
+
+
+ : null}
+ >
+ {iniciando ? 'Iniciando...' : 'Iniciar Coleta'}
+
+
+
+
+
+
+
+
+
+ );
+};
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;
+ }
+}