Compare commits
1
Commits
novo_provas
...
webproof
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e75018a35 |
No files matched your search
@@ -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[]
|
||||||
|
// }
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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();
|
||||||
@@ -1482,3 +1483,4 @@ 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
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user