revert commit

This commit is contained in:
rodrigo committed 2025-07-27 18:50:37 -03:00
1 parent 24b5d04fbc
commit 928dfc0443
13 files changed
+11 -1506

No files matched your search

+4 -11
View File
@@ -29,7 +29,7 @@
"@mui/icons-material": "^5.15.6",
"@mui/material": "^5.15.6",
"@mui/x-date-pickers": "^6.19.0",
"@prisma/client": "^6.12.0",
"@prisma/client": "^6.5.0",
"@tinymce/tinymce-react": "^6.1.0",
"@tiptap/extension-link": "^2.11.7",
"@tiptap/react": "^2.11.7",
@@ -67,12 +67,7 @@
"uuid": "^9.0.1",
"webdav": "^5.3.1",
"xlsx": "^0.18.5",
"zod": "^3.22.4",
"react-webcam": "^7.2.0",
"recordrtc": "^5.6.2",
"html2canvas": "^1.4.1",
"canvas": "^3.1.2",
"ffmpeg-static": "^5.2.0"
"zod": "^3.22.4"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.1",
@@ -98,13 +93,11 @@
"eslint-plugin-react-hooks": "^5.2.0",
"postcss": "^8.4.33",
"prettier": "^3.5.3",
"prisma": "^6.12.0",
"prisma": "^6.5.0",
"tailwindcss": "^3.4.1",
"ts-node": "^10.9.2",
"typescript": "^5.8.2",
"vite": "^6.2.4",
"@types/recordrtc": "^5.6.11",
"@types/html2canvas": "^1.0.0"
"vite": "^6.2.4"
},
"prisma": {
"seed": "ts-node prisma/seed.ts"
-25
View File
@@ -1,25 +0,0 @@
-- CreateTable
CREATE TABLE "SessaoProva" (
"id" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'ATIVA',
"dadosSessao" TEXT NOT NULL,
"dataInicio" TIMESTAMP(3) NOT NULL,
"dataFim" TIMESTAMP(3),
"videoPath" TEXT,
"relatorioPath" TEXT,
"hashSessao" TEXT,
"metadata" JSONB,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SessaoProva_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "SessaoProva_hashSessao_idx" ON "SessaoProva"("hashSessao");
-- CreateIndex
CREATE INDEX "SessaoProva_status_idx" ON "SessaoProva"("status");
-- CreateIndex
CREATE INDEX "SessaoProva_dataInicio_idx" ON "SessaoProva"("dataInicio");
-19
View File
@@ -1067,22 +1067,3 @@ model CalculoPensaoAlimenticia {
@@map("calculos_pensao_alimenticia")
}
model SessaoProva {
id String @id @default(cuid())
status String @default("ATIVA") // ATIVA, PROCESSANDO, CONCLUIDA, ERRO
dadosSessao String // JSON com dados da sessão
dataInicio DateTime
dataFim DateTime?
videoPath String?
relatorioPath String?
hashSessao String? @unique
metadata Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([hashSessao])
@@index([status])
@@index([dataInicio])
@@map("sessao_prova")
}
-16
View File
@@ -95,7 +95,6 @@ const router = createBrowserRouter(
element={<VerificarProva />}
errorElement={<ErrorBoundary />}
/>
{/* Rotas para Cálculos Judiciais */}
<Route path="/calculos" element={
<ProtectedRoute>
@@ -149,19 +148,4 @@ export const App: React.FC = () => {
);
};
export default App;
return (
<QueryClientProvider client={queryClient}>
<LocalizationProvider dateAdapter={AdapterDateFns} adapterLocale={ptBR}>
<AuthProvider>
<ThemeProvider>
<RouterProvider router={router} />
<ToastContainer />
</ThemeProvider>
</AuthProvider>
</LocalizationProvider>
</QueryClientProvider>
);
};
export default App;
+2 -91
View File
@@ -1,95 +1,6 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { DashboardLayout } from './dashboard/DashboardLayout';
export const Dashboard: React.FC = () => {
return (
<div className="p-6">
<div className="mb-8">
<h1 className="text-2xl font-bold text-gray-900">Dashboard</h1>
<p className="text-gray-600">Visão geral do seu escritório</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
{/* Widget para sessões de prova */}
<div className="bg-white rounded-lg shadow p-6">
<div className="flex items-center">
<div className="flex-shrink-0">
<svg
className="h-8 w-8 text-purple-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"
/>
</svg>
</div>
<div className="ml-5 w-0 flex-1">
<dl>
<dt className="text-sm font-medium text-gray-500 truncate">
Sessões de Gravação
</dt>
<dd className="text-lg font-medium text-gray-900">
Disponível
</dd>
</dl>
</div>
</div>
<div className="mt-3">
<Link
to="/sessions/new"
className="text-sm text-purple-600 hover:text-purple-500"
>
Nova sessão de coleta →
</Link>
</div>
</div>
{/* Widget para provas digitais */}
<div className="bg-white rounded-lg shadow p-6">
<div className="flex items-center">
<div className="flex-shrink-0">
<svg
className="h-8 w-8 text-blue-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
/>
</svg>
</div>
<div className="ml-5 w-0 flex-1">
<dl>
<dt className="text-sm font-medium text-gray-500 truncate">
Provas Digitais
</dt>
<dd className="text-lg font-medium text-gray-900">
Ativo
</dd>
</dl>
</div>
</div>
<div className="mt-3">
<Link
to="/provas/upload"
className="text-sm text-blue-600 hover:text-blue-500"
>
Validar documento →
</Link>
</div>
</div>
</div>
</div>
);
return <DashboardLayout />;
};
export default Dashboard;
-9
View File
@@ -116,15 +116,6 @@ export const MainLayout: React.FC = () => {
icon: MoneyIcon
}
]
},
{
label: 'Provas Digitais',
icon: ShieldCheckIcon,
subitems: [
{ label: 'Upload de Documentos', path: '/provas/upload' },
{ label: 'Gravação de Sessão', path: '/sessions/new' },
{ label: 'Administrar Provas', path: '/provas/admin', roles: ['ADVOGADO_ADMIN'] },
]
}
];
-387
View File
@@ -1,387 +0,0 @@
import React, { useState, useRef, useCallback, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify';
export const SessionRecording: React.FC = () => {
const [recording, setRecording] = useState(false);
const [sessionId, setSessionId] = useState<string | null>(null);
const [recordingTime, setRecordingTime] = useState(0);
const [screenshots, setScreenshots] = useState<string[]>([]);
const [isProcessing, setIsProcessing] = useState(false);
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
const streamRef = useRef<MediaStream | null>(null);
const recordedChunksRef = useRef<Blob[]>([]);
const timerRef = useRef<NodeJS.Timeout | null>(null);
const navigate = useNavigate();
useEffect(() => {
return () => {
if (timerRef.current) {
clearInterval(timerRef.current);
}
if (streamRef.current) {
streamRef.current.getTracks().forEach(track => track.stop());
}
};
}, []);
const startRecording = useCallback(async () => {
try {
// Coletar informações de contexto
const metadata = await collectContextMetadata();
// Criar sessão no backend
const response = await fetch('/api/sessions/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(metadata)
});
if (!response.ok) {
throw new Error('Erro ao criar sessão');
}
const { sessionId: newSessionId } = await response.json();
setSessionId(newSessionId);
// Solicitar captura de tela
const stream = await navigator.mediaDevices.getDisplayMedia({
video: { mediaSource: 'screen' },
audio: true
});
streamRef.current = stream;
// Configurar MediaRecorder
const mediaRecorder = new MediaRecorder(stream, {
mimeType: 'video/webm;codecs=vp9'
});
mediaRecorderRef.current = mediaRecorder;
recordedChunksRef.current = [];
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
recordedChunksRef.current.push(event.data);
}
};
mediaRecorder.onstop = () => {
handleRecordingStop(newSessionId);
};
// Iniciar gravação
mediaRecorder.start(1000); // Gravar em chunks de 1 segundo
setRecording(true);
setRecordingTime(0);
// Iniciar timer
timerRef.current = setInterval(() => {
setRecordingTime(prev => prev + 1);
}, 1000);
// Overlay de informações na tela
createScreenOverlay(metadata);
toast.success('Gravação de sessão iniciada!');
} catch (error) {
console.error('Erro ao iniciar gravação:', error);
toast.error('Erro ao iniciar gravação: ' + error.message);
}
}, []);
const stopRecording = useCallback(() => {
if (mediaRecorderRef.current && recording) {
mediaRecorderRef.current.stop();
setRecording(false);
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
if (streamRef.current) {
streamRef.current.getTracks().forEach(track => track.stop());
}
removeScreenOverlay();
}
}, [recording]);
const takeScreenshot = useCallback(async () => {
if (!sessionId || !streamRef.current) {
toast.error('Sessão não ativa');
return;
}
try {
// Capturar screenshot usando Canvas
const canvas = document.createElement('canvas');
const video = document.createElement('video');
video.srcObject = streamRef.current;
video.play();
video.onloadedmetadata = async () => {
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const ctx = canvas.getContext('2d');
ctx?.drawImage(video, 0, 0);
// Converter para base64
const screenshotData = canvas.toDataURL('image/png');
// Enviar para o backend
const response = await fetch(`/api/sessions/${sessionId}/screenshot`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ screenshot: screenshotData })
});
if (response.ok) {
const { filename } = await response.json();
setScreenshots(prev => [...prev, filename]);
toast.success('Screenshot capturado!');
}
};
} catch (error) {
console.error('Erro ao capturar screenshot:', error);
toast.error('Erro ao capturar screenshot');
}
}, [sessionId]);
const handleRecordingStop = async (sessionId: string) => {
try {
setIsProcessing(true);
// Combinar chunks do vídeo
const blob = new Blob(recordedChunksRef.current, { type: 'video/webm' });
// Enviar vídeo para o backend
const formData = new FormData();
formData.append('video', blob, `session_${sessionId}.webm`);
const uploadResponse = await fetch(`/api/sessions/${sessionId}/video`, {
method: 'POST',
body: formData
});
if (!uploadResponse.ok) {
throw new Error('Erro ao enviar vídeo');
}
// Finalizar sessão e gerar relatório
const finalizeResponse = await fetch(`/api/sessions/${sessionId}/finalize`, {
method: 'POST'
});
if (!finalizeResponse.ok) {
throw new Error('Erro ao finalizar sessão');
}
const { reportPath } = await finalizeResponse.json();
toast.success('Sessão finalizada e relatório gerado!');
// Navegar para página de resultados
navigate(`/sessions/report/${sessionId}`);
} catch (error) {
console.error('Erro ao processar gravação:', error);
toast.error('Erro ao processar gravação: ' + error.message);
} finally {
setIsProcessing(false);
}
};
const formatTime = (seconds: number): string => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
};
return (
<div className="max-w-4xl mx-auto p-6">
<div className="bg-white rounded-lg shadow-lg p-6">
<h2 className="text-2xl font-bold text-gray-900 mb-6">
Gravação de Sessão de Coleta de Prova
</h2>
<div className="space-y-6">
{/* Status da gravação */}
<div className="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
<div className="flex items-center space-x-3">
<div className={`w-3 h-3 rounded-full ${recording ? 'bg-red-500 animate-pulse' : 'bg-gray-300'}`} />
<span className="font-medium">
{recording ? 'Gravando' : isProcessing ? 'Processando...' : 'Pronto para gravar'}
</span>
{recording && (
<span className="font-mono text-lg text-red-600">
{formatTime(recordingTime)}
</span>
)}
</div>
{sessionId && (
<span className="text-sm text-gray-500">
Sessão: {sessionId}
</span>
)}
</div>
{/* Controles */}
<div className="flex justify-center space-x-4">
{!recording && !isProcessing && (
<button
onClick={startRecording}
className="px-6 py-3 bg-red-600 text-white rounded-lg hover:bg-red-700 font-medium"
>
Iniciar Gravação
</button>
)}
{recording && (
<>
<button
onClick={takeScreenshot}
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium"
>
Capturar Tela
</button>
<button
onClick={stopRecording}
className="px-6 py-3 bg-gray-600 text-white rounded-lg hover:bg-gray-700 font-medium"
>
Parar Gravação
</button>
</>
)}
</div>
{/* Informações da sessão */}
{(recording || screenshots.length > 0) && (
<div className="border-t pt-4">
<h3 className="font-medium text-gray-900 mb-2">Informações da Sessão</h3>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-gray-500">Screenshots capturados:</span>
<span className="ml-2 font-medium">{screenshots.length}</span>
</div>
<div>
<span className="text-gray-500">Tempo de gravação:</span>
<span className="ml-2 font-medium">{formatTime(recordingTime)}</span>
</div>
</div>
</div>
)}
{/* Lista de screenshots */}
{screenshots.length > 0 && (
<div className="border-t pt-4">
<h3 className="font-medium text-gray-900 mb-2">Screenshots Capturados</h3>
<div className="space-y-1">
{screenshots.map((screenshot, index) => (
<div key={index} className="text-sm text-gray-600">
{index + 1}. {screenshot}
</div>
))}
</div>
</div>
)}
{isProcessing && (
<div className="text-center py-8">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4" />
<p className="text-gray-600">Processando gravação e gerando relatório...</p>
</div>
)}
</div>
</div>
</div>
);
};
// Função para coletar metadados de contexto
async function collectContextMetadata() {
const getLocation = (): Promise<GeolocationPosition | null> => {
return new Promise((resolve) => {
if (!navigator.geolocation) {
resolve(null);
return;
}
navigator.geolocation.getCurrentPosition(
(position) => resolve(position),
() => resolve(null),
{ timeout: 5000 }
);
});
};
const location = await getLocation();
return {
userAgent: navigator.userAgent,
language: navigator.language,
platform: navigator.platform,
cookieEnabled: navigator.cookieEnabled,
onlineStatus: navigator.onLine,
screenResolution: `${screen.width}x${screen.height}`,
viewportSize: `${window.innerWidth}x${window.innerHeight}`,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
geolocation: location ? {
latitude: location.coords.latitude,
longitude: location.coords.longitude,
accuracy: location.coords.accuracy
} : null,
timestamp: new Date().toISOString(),
url: window.location.href,
referrer: document.referrer
};
}
// Função para criar overlay na tela
function createScreenOverlay(metadata: any) {
const overlay = document.createElement('div');
overlay.id = 'recording-overlay';
overlay.style.cssText = `
position: fixed;
top: 10px;
right: 10px;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 10px;
border-radius: 5px;
font-family: monospace;
font-size: 12px;
z-index: 10000;
max-width: 300px;
`;
overlay.innerHTML = `
<div>🔴 GRAVAÇÃO ATIVA</div>
<div>IP: ${metadata.ipAddress || 'Coletando...'}</div>
<div>Timestamp: ${new Date().toLocaleString()}</div>
<div>User-Agent: ${metadata.userAgent.substring(0, 50)}...</div>
`;
document.body.appendChild(overlay);
}
// Função para remover overlay
function removeScreenOverlay() {
const overlay = document.getElementById('recording-overlay');
if (overlay) {
overlay.remove();
}
}
export default SessionRecording;
-227
View File
@@ -1,227 +0,0 @@
import React, { useState, useEffect } from 'react';
import { useParams, Link } from 'react-router-dom';
import { format } from 'date-fns';
import { ptBR } from 'date-fns/locale';
interface SessionData {
id: string;
startTime: string;
endTime?: string;
ipAddress: string;
dnsInfo: string;
userAgent: string;
geolocation?: {
latitude: number;
longitude: number;
};
screenshots: string[];
videoPath?: string;
metadata: any;
}
export const SessionReport: React.FC = () => {
const { sessionId } = useParams<{ sessionId: string }>();
const [session, setSession] = useState<SessionData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchSession = async () => {
try {
const response = await fetch(`/api/sessions/${sessionId}`);
if (!response.ok) {
throw new Error('Sessão não encontrada');
}
const data = await response.json();
setSession(data.session);
} catch (error) {
console.error('Erro ao buscar sessão:', error);
setError(error.message);
} finally {
setLoading(false);
}
};
if (sessionId) {
fetchSession();
}
}, [sessionId]);
const formatDuration = (start: string, end?: string) => {
if (!end) return 'Em andamento';
const startTime = new Date(start);
const endTime = new Date(end);
const diffMs = endTime.getTime() - startTime.getTime();
const hours = Math.floor(diffMs / (1000 * 60 * 60));
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((diffMs % (1000 * 60)) / 1000);
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
};
if (loading) {
return (
<div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-4xl mx-auto">
<div className="bg-white rounded-lg shadow p-6">
<div className="animate-pulse">
<div className="h-6 bg-gray-300 rounded w-3/4 mb-4"></div>
<div className="h-4 bg-gray-300 rounded w-1/2 mb-2"></div>
<div className="h-4 bg-gray-300 rounded w-2/3"></div>
</div>
</div>
</div>
</div>
);
}
if (error || !session) {
return (
<div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-4xl mx-auto">
<div className="bg-white rounded-lg shadow p-6 text-center">
<div className="text-red-600 text-xl font-bold mb-4">
Erro ao carregar sessão
</div>
<p className="text-gray-600 mb-6">{error || 'Sessão não encontrada'}</p>
<Link
to="/sessions/new"
className="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Nova Sessão
</Link>
</div>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-4xl mx-auto">
<div className="bg-white rounded-lg shadow overflow-hidden">
{/* Header */}
<div className="bg-green-600 text-white px-6 py-4">
<h1 className="text-2xl font-bold">Relatório de Sessão de Coleta de Prova</h1>
<p className="text-green-100">ID da Sessão: {session.id}</p>
</div>
{/* Resumo da sessão */}
<div className="p-6 border-b border-gray-200">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Resumo da Sessão</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h3 className="text-sm font-medium text-gray-500">Data/Hora de Início</h3>
<p className="text-lg text-gray-900">
{format(new Date(session.startTime), 'dd/MM/yyyy HH:mm:ss', { locale: ptBR })}
</p>
</div>
{session.endTime && (
<div>
<h3 className="text-sm font-medium text-gray-500">Data/Hora de Fim</h3>
<p className="text-lg text-gray-900">
{format(new Date(session.endTime), 'dd/MM/yyyy HH:mm:ss', { locale: ptBR })}
</p>
</div>
)}
<div>
<h3 className="text-sm font-medium text-gray-500">Duração</h3>
<p className="text-lg text-gray-900">
{formatDuration(session.startTime, session.endTime)}
</p>
</div>
<div>
<h3 className="text-sm font-medium text-gray-500">Screenshots Capturados</h3>
<p className="text-lg text-gray-900">{session.screenshots.length}</p>
</div>
</div>
</div>
{/* Informações de rede */}
<div className="p-6 border-b border-gray-200">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Informações de Rede</h2>
<div className="space-y-4">
<div>
<h3 className="text-sm font-medium text-gray-500">Endereço IP</h3>
<p className="text-gray-900 font-mono">{session.ipAddress}</p>
</div>
<div>
<h3 className="text-sm font-medium text-gray-500">DNS Reverso</h3>
<p className="text-gray-900 font-mono">{session.dnsInfo}</p>
</div>
<div>
<h3 className="text-sm font-medium text-gray-500">User-Agent</h3>
<p className="text-gray-900 font-mono text-sm break-all">{session.userAgent}</p>
</div>
</div>
</div>
{/* Geolocalização */}
{session.geolocation && (
<div className="p-6 border-b border-gray-200">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Geolocalização</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<h3 className="text-sm font-medium text-gray-500">Latitude</h3>
<p className="text-gray-900 font-mono">{session.geolocation.latitude}</p>
</div>
<div>
<h3 className="text-sm font-medium text-gray-500">Longitude</h3>
<p className="text-gray-900 font-mono">{session.geolocation.longitude}</p>
</div>
</div>
</div>
)}
{/* Metadados adicionais */}
<div className="p-6 border-b border-gray-200">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Metadados do Sistema</h2>
<div className="bg-gray-50 rounded-lg p-4">
<pre className="text-sm text-gray-700 overflow-x-auto">
{JSON.stringify(session.metadata, null, 2)}
</pre>
</div>
</div>
{/* Ações */}
<div className="p-6 flex flex-col sm:flex-row gap-4">
<a
href={`/api/sessions/${session.id}/report`}
className="inline-flex items-center justify-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
>
Baixar Relatório PDF
</a>
{session.videoPath && (
<button className="inline-flex items-center justify-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
Visualizar Vídeo
</button>
)}
<Link
to="/sessions/new"
className="inline-flex items-center justify-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
>
Nova Sessão
</Link>
</div>
</div>
<div className="mt-4 text-center text-sm text-gray-500">
Relatório gerado pelo sistema AdvX Provas - Coleta de Evidências Digitais
</div>
</div>
</div>
);
};
export default SessionReport;
-51
View File
@@ -1,51 +0,0 @@
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import { prisma } from '../config/database';
export interface SessionRequest extends Request {
user?: {
id: string;
email: string;
perfil: string;
};
}
export const sessionAuthMiddleware = async (
req: SessionRequest,
res: Response,
next: NextFunction
) => {
try {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
// Para sessões, permitir acesso sem autenticação em certas rotas
if (req.path.includes('/public/') || req.method === 'GET') {
return next();
}
return res.status(401).json({ error: 'Token não fornecido' });
}
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'default-secret') as any;
const user = await prisma.usuario.findUnique({
where: { id: decoded.id },
select: {
id: true,
email: true,
perfil: true,
ativo: true
}
});
if (!user || !user.ativo) {
return res.status(401).json({ error: 'Usuário inválido ou inativo' });
}
req.user = user;
next();
} catch (error) {
console.error('Erro na autenticação de sessão:', error);
res.status(401).json({ error: 'Token inválido' });
}
};
+5 -7
View File
@@ -24,10 +24,9 @@ import processosAdministrativosRoutes from './routes/processosAdministrativos';
import provasRouter from './routes/provas';
import calculosJudiciaisRoutes from './routes/calculosJudiciais';
import indicesMonetariosRoutes from './routes/indicesMonetarios';
import pensaoAlimenticiaRouter from './routes/pensaoAlimenticia';
import financeiroPainelRoutes from './routes/financeiroPainel';
import audienciasRoutes from './routes/audiencias';
import sessionsRouter from './routes/sessions'; // Adicionar importação das rotas de sessão
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
const app = express();
@@ -1472,7 +1471,6 @@ app.patch("/api/atendimentos/:id/status", authMiddleware, async (req, res) => {
app.use('/api/anexos', anexosRoutes);
app.use('/api/atendimentos', atendimentosRoutes);
app.use('/api/provas', provasRouter);
app.use('/api/sessions', sessionsRouter); // Adicionar rotas de sessão
app.use('/api/tarefas', tarefasRoutes);
app.use('/api/dashboard', authMiddleware, dashboardRoutes);
@@ -1482,5 +1480,5 @@ app.use('/api/processos-administrativos', processosAdministrativosRoutes);
app.use('/api/calculos', calculosJudiciaisRoutes);
app.use('/api/indices', indicesMonetariosRoutes);
app.use('/api/pensao-alimenticia', pensaoAlimenticiaRouter);
app.use('/api/financeiro-painel', financeiroPainelRoutes);
app.use('/api/audiencias', audienciasRoutes);
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
-254
View File
@@ -1,254 +0,0 @@
import express from 'express';
import multer from 'multer';
import { SessionProvaService } from '../../services/SessionProvaService';
import { prisma } from '../prismaClient';
import * as path from 'path';
import * as fs from 'fs';
const router = express.Router();
const upload = multer({ storage: multer.memoryStorage() });
const sessionService = new SessionProvaService();
// Criar nova sessão de gravação
router.post('/create', async (req, res) => {
try {
// Validação de entrada
if (!req.body || typeof req.body !== 'object') {
return res.status(400).json({ error: 'Dados de entrada inválidos' });
}
// Coletar IP real
const realIP = req.headers['x-forwarded-for']?.toString().split(',')[0].trim() ||
req.ip ||
req.socket?.remoteAddress ||
'127.0.0.1';
const metadata = {
...req.body,
ipAddress: realIP,
serverTimestamp: new Date().toISOString(),
headers: {
'user-agent': req.headers['user-agent'],
'accept-language': req.headers['accept-language'],
'referer': req.headers['referer'],
'host': req.headers['host']
}
};
const sessionData = await sessionService.createSession(metadata);
res.json({
success: true,
sessionId: sessionData.id,
metadata: sessionData.metadata
});
} catch (error) {
console.error('Erro ao criar sessão:', error);
res.status(500).json({
error: 'Erro ao criar sessão de gravação',
details: error.message
});
}
});
// Adicionar screenshot à sessão
router.post('/:sessionId/screenshot', async (req, res) => {
try {
const { sessionId } = req.params;
const { screenshot } = req.body;
if (!screenshot) {
return res.status(400).json({ error: 'Screenshot não fornecido' });
}
if (!sessionId || typeof sessionId !== 'string') {
return res.status(400).json({ error: 'ID de sessão inválido' });
}
const filename = await sessionService.addScreenshot(sessionId, screenshot);
res.json({
success: true,
filename,
timestamp: new Date().toISOString()
});
} catch (error) {
console.error('Erro ao salvar screenshot:', error);
res.status(500).json({
error: 'Erro ao salvar screenshot',
details: error.message
});
}
});
// Upload do vídeo da sessão
router.post('/:sessionId/video', upload.single('video'), async (req, res) => {
try {
const { sessionId } = req.params;
if (!req.file) {
return res.status(400).json({ error: 'Vídeo não fornecido' });
}
if (!sessionId || typeof sessionId !== 'string') {
return res.status(400).json({ error: 'ID de sessão inválido' });
}
const filename = await sessionService.saveSessionVideo(sessionId, req.file.buffer);
res.json({
success: true,
filename,
size: req.file.size,
timestamp: new Date().toISOString()
});
} catch (error) {
console.error('Erro ao salvar vídeo:', error);
res.status(500).json({
error: 'Erro ao salvar vídeo',
details: error.message
});
}
});
// Finalizar sessão e gerar relatório
router.post('/:sessionId/finalize', async (req, res) => {
try {
const { sessionId } = req.params;
if (!sessionId || typeof sessionId !== 'string') {
return res.status(400).json({ error: 'ID de sessão inválido' });
}
const reportPath = await sessionService.finalizeSession(sessionId);
res.json({
success: true,
sessionId,
reportPath,
timestamp: new Date().toISOString()
});
} catch (error) {
console.error('Erro ao finalizar sessão:', error);
res.status(500).json({
error: 'Erro ao finalizar sessão',
details: error.message
});
}
});
// Obter informações da sessão
router.get('/:sessionId', async (req, res) => {
try {
const { sessionId } = req.params;
if (!sessionId || typeof sessionId !== 'string') {
return res.status(400).json({ error: 'ID de sessão inválido' });
}
const sessionData = await sessionService.getSession(sessionId);
if (!sessionData) {
return res.status(404).json({ error: 'Sessão não encontrada' });
}
res.json({
success: true,
session: sessionData
});
} catch (error) {
console.error('Erro ao buscar sessão:', error);
res.status(500).json({
error: 'Erro ao buscar sessão',
details: error.message
});
}
});
// Baixar relatório da sessão
router.get('/:sessionId/report', async (req, res) => {
try {
const { sessionId } = req.params;
if (!sessionId || typeof sessionId !== 'string') {
return res.status(400).json({ error: 'ID de sessão inválido' });
}
const session = await prisma.sessaoProva.findUnique({
where: { id: sessionId }
});
if (!session || !session.relatorioPath) {
return res.status(404).json({ error: 'Relatório não encontrado' });
}
const reportPath = session.relatorioPath;
if (!fs.existsSync(reportPath)) {
return res.status(404).json({ error: 'Arquivo do relatório não encontrado' });
}
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', `attachment; filename="relatorio_sessao_${sessionId}.pdf"`);
const fileStream = fs.createReadStream(reportPath);
fileStream.pipe(res);
} catch (error) {
console.error('Erro ao servir relatório:', error);
res.status(500).json({
error: 'Erro ao baixar relatório',
details: error.message
});
}
});
// Verificar sessão por hash
router.get('/verify/:hash', async (req, res) => {
try {
const { hash } = req.params;
if (!hash || typeof hash !== 'string') {
return res.status(400).json({ error: 'Hash inválido' });
}
const result = await sessionService.verifySession(hash);
res.json(result);
} catch (error) {
console.error('Erro ao verificar sessão:', error);
res.status(500).json({
error: 'Erro ao verificar sessão',
details: error.message
});
}
});
// Listar sessões (admin)
router.get('/admin/list', async (req, res) => {
try {
const sessions = await prisma.sessaoProva.findMany({
orderBy: { dataInicio: 'desc' },
take: 50
});
res.json({
success: true,
sessions: sessions.map(session => ({
id: session.id,
status: session.status,
dataInicio: session.dataInicio,
dataFim: session.dataFim,
hashSessao: session.hashSessao,
temRelatorio: !!session.relatorioPath
}))
});
} catch (error) {
console.error('Erro ao listar sessões:', error);
res.status(500).json({
error: 'Erro ao listar sessões',
details: error.message
});
}
});
export default router;
-48
View File
@@ -4,7 +4,6 @@ import { createHash } from 'crypto';
import { PDFDocument, StandardFonts } from 'pdf-lib';
import * as OpenTimestamps from 'opentimestamps';
import sharp from 'sharp';
import { promises as fs } from 'fs';
export class ProvaService {
private fileValidator: FileValidator;
@@ -97,51 +96,4 @@ export class ProvaService {
buffer: await pdfDoc.save()
};
}
async verifyFile(hash: string) {
try {
const prova = await prisma.prova.findFirst({
where: { checksumSha256: hash }
});
if (!prova) {
return {
verified: false,
message: 'Documento não encontrado ou hash inválido'
};
}
// Verificar se o arquivo ainda existe
const filePath = prova.filePath;
if (filePath) {
try {
await fs.access(filePath);
} catch {
return {
verified: false,
message: 'Arquivo original não encontrado'
};
}
}
return {
verified: true,
message: 'Documento verificado com sucesso',
prova: {
fileName: prova.fileName,
checksumSha256: prova.checksumSha256,
checksumSha512: prova.checksumSha512,
checksumMd5: prova.checksumMd5,
timestamp: prova.timestamp,
metadata: prova.metadata,
dataCriacao: prova.dataCriacao,
dataValida: prova.dataAtualizacao
},
timestamp: prova.timestamp ? JSON.parse(prova.timestamp) : null
};
} catch (error) {
console.error('Erro ao verificar arquivo:', error);
throw new Error(`Erro ao verificar arquivo: ${error.message}`);
}
}
}
-361
View File
@@ -1,361 +0,0 @@
import { prisma } from '../server/prismaClient';
import { createHash } from 'crypto';
import { PDFDocument, StandardFonts, rgb } from 'pdf-lib';
import * as fs from 'fs/promises';
import * as path from 'path';
import { format } from 'date-fns';
import { ptBR } from 'date-fns/locale';
export interface SessionData {
id: string;
userAgent: string;
ipAddress: string;
dnsInfo: string;
geolocation?: {
latitude: number;
longitude: number;
};
startTime: Date;
endTime?: Date;
screenshots: string[];
videoPath?: string;
metadata: any;
}
export class SessionProvaService {
private storageBasePath = path.join(process.cwd(), 'storage', 'sessions');
constructor() {
this.ensureStorageDirectory();
}
private async ensureStorageDirectory() {
try {
await fs.access(this.storageBasePath);
} catch {
await fs.mkdir(this.storageBasePath, { recursive: true });
}
}
async createSession(metadata: any): Promise<SessionData> {
const sessionId = this.generateSessionId();
// Coletar informações de DNS
const dnsInfo = await this.collectDNSInfo(metadata.ipAddress);
const sessionData: SessionData = {
id: sessionId,
userAgent: metadata.userAgent,
ipAddress: metadata.ipAddress,
dnsInfo,
geolocation: metadata.geolocation,
startTime: new Date(),
screenshots: [],
metadata: {
...metadata,
sessionId,
browserInfo: this.parseBrowserInfo(metadata.userAgent),
timestamp: new Date().toISOString()
}
};
// Salvar no banco de dados
await prisma.sessaoProva.create({
data: {
id: sessionId,
status: 'ATIVA',
dadosSessao: JSON.stringify(sessionData),
dataInicio: sessionData.startTime,
metadata: sessionData.metadata
}
});
return sessionData;
}
async addScreenshot(sessionId: string, screenshotData: string): Promise<string> {
const timestamp = Date.now();
const filename = `screenshot_${timestamp}.png`;
const filepath = path.join(this.storageBasePath, sessionId, filename);
// Criar diretório da sessão se não existir
await fs.mkdir(path.dirname(filepath), { recursive: true });
// Converter base64 para buffer e salvar
const buffer = Buffer.from(screenshotData.replace(/^data:image\/png;base64,/, ''), 'base64');
await fs.writeFile(filepath, buffer);
// Atualizar sessão no banco
const session = await prisma.sessaoProva.findUnique({
where: { id: sessionId }
});
if (session) {
const sessionData = JSON.parse(session.dadosSessao) as SessionData;
sessionData.screenshots.push(filename);
await prisma.sessaoProva.update({
where: { id: sessionId },
data: {
dadosSessao: JSON.stringify(sessionData)
}
});
}
return filename;
}
async saveSessionVideo(sessionId: string, videoBlob: Buffer): Promise<string> {
const filename = `session_${sessionId}.webm`;
const filepath = path.join(this.storageBasePath, sessionId, filename);
await fs.mkdir(path.dirname(filepath), { recursive: true });
await fs.writeFile(filepath, videoBlob);
// Atualizar sessão
await prisma.sessaoProva.update({
where: { id: sessionId },
data: {
videoPath: filename,
status: 'PROCESSANDO'
}
});
return filename;
}
async finalizeSession(sessionId: string): Promise<string> {
const session = await prisma.sessaoProva.findUnique({
where: { id: sessionId }
});
if (!session) {
throw new Error('Sessão não encontrada');
}
const sessionData = JSON.parse(session.dadosSessao) as SessionData;
sessionData.endTime = new Date();
// Gerar relatório PDF
const reportPath = await this.generateSessionReport(sessionId, sessionData);
// Gerar hash da sessão
const sessionHash = this.generateSessionHash(sessionData);
// Finalizar no banco
await prisma.sessaoProva.update({
where: { id: sessionId },
data: {
status: 'CONCLUIDA',
dataFim: sessionData.endTime,
dadosSessao: JSON.stringify(sessionData),
relatorioPath: reportPath,
hashSessao: sessionHash
}
});
return reportPath;
}
private async generateSessionReport(sessionId: string, sessionData: SessionData): Promise<string> {
try {
const pdfDoc = await PDFDocument.create();
const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
const boldFont = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
// Primeira página - Screenshots (se houver)
if (sessionData.screenshots.length > 0) {
for (const screenshot of sessionData.screenshots) {
const page = pdfDoc.addPage();
const screenshotPath = path.join(this.storageBasePath, sessionId, screenshot);
try {
const imageBuffer = await fs.readFile(screenshotPath);
const pngImage = await pdfDoc.embedPng(imageBuffer);
const { width, height } = page.getSize();
const imgDims = pngImage.scale(Math.min(width / pngImage.width, height / pngImage.height) * 0.8);
page.drawImage(pngImage, {
x: (width - imgDims.width) / 2,
y: (height - imgDims.height) / 2,
width: imgDims.width,
height: imgDims.height
});
// Adicionar timestamp no screenshot
const timestampFromFilename = screenshot.split('_')[1]?.split('.')[0];
if (timestampFromFilename) {
const screenshotDate = new Date(parseInt(timestampFromFilename));
page.drawText(`Capturado em: ${format(screenshotDate, 'dd/MM/yyyy HH:mm:ss', { locale: ptBR })}`, {
x: 50,
y: 50,
size: 10,
font,
color: rgb(0, 0, 0)
});
}
} catch (error) {
console.error('Erro ao processar screenshot:', error);
}
}
}
// Página de relatório detalhado
const reportPage = pdfDoc.addPage();
const { width, height } = reportPage.getSize();
let yPosition = height - 50;
// Título
reportPage.drawText('RELATÓRIO DE SESSÃO DE COLETA DE PROVA DIGITAL', {
x: 50,
y: yPosition,
size: 16,
font: boldFont,
color: rgb(0, 0, 0.8)
});
yPosition -= 40;
// Informações da sessão - evitar JSON.stringify que pode causar problemas
const sessionInfo = [
`ID da Sessão: ${sessionData.id}`,
`Data/Hora de Início: ${format(sessionData.startTime, 'dd/MM/yyyy HH:mm:ss', { locale: ptBR })}`,
`Data/Hora de Fim: ${sessionData.endTime ? format(sessionData.endTime, 'dd/MM/yyyy HH:mm:ss', { locale: ptBR }) : 'Em andamento'}`,
`Duração: ${sessionData.endTime ? this.calculateDuration(sessionData.startTime, sessionData.endTime) : 'Em andamento'}`,
``,
`INFORMAÇÕES DE REDE:`,
`Endereço IP: ${sessionData.ipAddress}`,
`DNS: ${sessionData.dnsInfo}`,
`User-Agent: ${sessionData.userAgent.substring(0, 100)}...`,
``
];
if (sessionData.geolocation) {
sessionInfo.push(
`GEOLOCALIZAÇÃO:`,
`Latitude: ${sessionData.geolocation.latitude}`,
`Longitude: ${sessionData.geolocation.longitude}`,
``
);
}
sessionInfo.push(
`CAPTURAS DE TELA: ${sessionData.screenshots.length}`,
`VÍDEO DA SESSÃO: ${sessionData.videoPath ? 'Disponível' : 'Não disponível'}`,
``,
`DADOS TÉCNICOS:`,
`Browser: ${sessionData.metadata?.browserInfo?.name || 'N/A'} ${sessionData.metadata?.browserInfo?.version || ''}`,
`Timestamp: ${sessionData.metadata?.timestamp || 'N/A'}`
);
// Desenhar informações
for (const info of sessionInfo) {
if (yPosition < 100) {
const newPage = pdfDoc.addPage();
yPosition = newPage.getSize().height - 50;
}
reportPage.drawText(info, {
x: 50,
y: yPosition,
size: 10,
font: info.includes(':') && info.endsWith(':') ? boldFont : font,
color: rgb(0, 0, 0)
});
yPosition -= 15;
}
// Salvar PDF
const pdfBytes = await pdfDoc.save();
const reportPath = path.join(this.storageBasePath, sessionId, `relatorio_${sessionId}.pdf`);
await fs.writeFile(reportPath, pdfBytes);
return reportPath;
} catch (error) {
console.error('Erro ao gerar relatório da sessão:', error);
throw new Error(`Erro ao gerar relatório: ${error.message}`);
}
}
private generateSessionId(): string {
return `sess_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
private generateSessionHash(sessionData: SessionData): string {
const dataToHash = JSON.stringify({
id: sessionData.id,
startTime: sessionData.startTime,
endTime: sessionData.endTime,
ipAddress: sessionData.ipAddress,
userAgent: sessionData.userAgent,
screenshots: sessionData.screenshots
});
return createHash('sha256').update(dataToHash).digest('hex');
}
private async collectDNSInfo(ipAddress: string): Promise<string> {
try {
const dns = require('dns').promises;
const hostnames = await dns.reverse(ipAddress);
return hostnames.join(', ');
} catch (error) {
return 'DNS não resolvido';
}
}
private parseBrowserInfo(userAgent: string): any {
// Parser simples de user agent
const browsers = [
{ name: 'Chrome', pattern: /Chrome\/([0-9.]+)/ },
{ name: 'Firefox', pattern: /Firefox\/([0-9.]+)/ },
{ name: 'Safari', pattern: /Safari\/([0-9.]+)/ },
{ name: 'Edge', pattern: /Edge\/([0-9.]+)/ }
];
for (const browser of browsers) {
const match = userAgent.match(browser.pattern);
if (match) {
return { name: browser.name, version: match[1] };
}
}
return { name: 'Desconhecido', version: 'N/A' };
}
private calculateDuration(start: Date, end: Date): string {
const diffMs = end.getTime() - start.getTime();
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
const diffMinutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
const diffSeconds = Math.floor((diffMs % (1000 * 60)) / 1000);
return `${diffHours.toString().padStart(2, '0')}:${diffMinutes.toString().padStart(2, '0')}:${diffSeconds.toString().padStart(2, '0')}`;
}
async getSession(sessionId: string): Promise<SessionData | null> {
const session = await prisma.sessaoProva.findUnique({
where: { id: sessionId }
});
if (!session) return null;
return JSON.parse(session.dadosSessao) as SessionData;
}
async verifySession(sessionHash: string): Promise<any> {
const session = await prisma.sessaoProva.findFirst({
where: { hashSessao: sessionHash }
});
if (!session) {
return { verified: false, message: 'Sessão não encontrada' };
}
return {
verified: true,
session: JSON.parse(session.dadosSessao),
status: session.status,
reportPath: session.relatorioPath
};
}
}