Compare commits

1 Commits
Author SHA1 Message Date
rodrigo 8e75018a35 add webproof system
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
2025-07-28 15:51:41 -03:00
6 changed files with 666 additions and 1 deletions

No files matched your search

+106
View File
@@ -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[]
// }
+6
View File
@@ -49,6 +49,7 @@ import { NovoCalculo } from './components/calculos/NovoCalculo';
import { DetalhesCalculo } from './components/calculos/DetalhesCalculo'; import { DetalhesCalculo } from './components/calculos/DetalhesCalculo';
import { NovoPensaoAlimenticia } from './components/calculos/NovoPensaoAlimenticia'; import { NovoPensaoAlimenticia } from './components/calculos/NovoPensaoAlimenticia';
import { DetalhePensaoAlimenticia } from './components/calculos/DetalhePensaoAlimenticia'; import { DetalhePensaoAlimenticia } from './components/calculos/DetalhePensaoAlimenticia';
import { IniciarNavegacao } from './components/navegacao/IniciarNavegacao';
const queryClient = new QueryClient(); const queryClient = new QueryClient();
@@ -126,6 +127,11 @@ const router = createBrowserRouter(
<DetalhePensaoAlimenticia /> <DetalhePensaoAlimenticia />
</ProtectedRoute> </ProtectedRoute>
} /> } />
<Route path="/navegacao/iniciar" element={
<ProtectedRoute>
<IniciarNavegacao />
</ProtectedRoute>
} />
</Route> </Route>
</Route> </Route>
<Route path="*" element={<Navigate to="/login" />} /> <Route path="*" element={<Navigate to="/login" />} />
@@ -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 (
<Container maxWidth="md">
<Box mt={4}>
<Card>
<CardContent>
<Typography variant="h4" gutterBottom>
Nova Sessão de Coleta de Provas
</Typography>
<Alert severity="info" sx={{ mb: 3 }}>
<Typography variant="body2">
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.
</Typography>
</Alert>
<Box component="form" sx={{ mt: 2 }}>
<TextField
fullWidth
label="Título da Sessão"
value={titulo}
onChange={(e) => setTitulo(e.target.value)}
margin="normal"
required
helperText="Descreva brevemente o objetivo desta coleta"
/>
<TextField
fullWidth
label="Descrição (opcional)"
value={descricao}
onChange={(e) => setDescricao(e.target.value)}
margin="normal"
multiline
rows={3}
helperText="Informações adicionais sobre o que será coletado"
/>
<Box mt={3} display="flex" gap={2}>
<Button
variant="contained"
color="primary"
onClick={handleIniciar}
disabled={iniciando || !titulo.trim()}
startIcon={iniciando ? <CircularProgress size={20} /> : null}
>
{iniciando ? 'Iniciando...' : 'Iniciar Coleta'}
</Button>
<Button
variant="outlined"
onClick={() => navigate('/navegacao')}
>
Cancelar
</Button>
</Box>
</Box>
</CardContent>
</Card>
</Box>
</Container>
);
};
+3 -1
View File
@@ -27,6 +27,7 @@ import indicesMonetariosRoutes from './routes/indicesMonetarios';
import pensaoAlimenticiaRouter from './routes/pensaoAlimenticia'; // Importar router de pensão alimentícia import pensaoAlimenticiaRouter from './routes/pensaoAlimenticia'; // Importar router de pensão alimentícia
import financeiroPainelRoutes from './routes/financeiroPainel'; // Importar rotas do financeiro painel import financeiroPainelRoutes from './routes/financeiroPainel'; // Importar rotas do financeiro painel
import audienciasRoutes from './routes/audiencias'; // Importar rotas de audiências import audienciasRoutes from './routes/audiencias'; // Importar rotas de audiências
import navegacaoRoutes from './routes/navegacao'; // Adicionar esta importação
const app = express(); const app = express();
@@ -1481,4 +1482,5 @@ app.use('/api/calculos', calculosJudiciaisRoutes);
app.use('/api/indices', indicesMonetariosRoutes); app.use('/api/indices', indicesMonetariosRoutes);
app.use('/api/pensao-alimenticia', pensaoAlimenticiaRouter); app.use('/api/pensao-alimenticia', pensaoAlimenticiaRouter);
app.use('/api/financeiro-painel', financeiroPainelRoutes); // Adicionar uso das rotas do financeiro painel 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 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
+168
View File
@@ -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;
+264
View File
@@ -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<string> {
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;
}
}