revert commit
This commit is contained in:
1 parent
24b5d04fbc
commit
928dfc0443
13 files changed
+12
-1507
No files matched your search
+5
-12
@@ -29,7 +29,7 @@
|
|||||||
"@mui/icons-material": "^5.15.6",
|
"@mui/icons-material": "^5.15.6",
|
||||||
"@mui/material": "^5.15.6",
|
"@mui/material": "^5.15.6",
|
||||||
"@mui/x-date-pickers": "^6.19.0",
|
"@mui/x-date-pickers": "^6.19.0",
|
||||||
"@prisma/client": "^6.12.0",
|
"@prisma/client": "^6.5.0",
|
||||||
"@tinymce/tinymce-react": "^6.1.0",
|
"@tinymce/tinymce-react": "^6.1.0",
|
||||||
"@tiptap/extension-link": "^2.11.7",
|
"@tiptap/extension-link": "^2.11.7",
|
||||||
"@tiptap/react": "^2.11.7",
|
"@tiptap/react": "^2.11.7",
|
||||||
@@ -67,12 +67,7 @@
|
|||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"webdav": "^5.3.1",
|
"webdav": "^5.3.1",
|
||||||
"xlsx": "^0.18.5",
|
"xlsx": "^0.18.5",
|
||||||
"zod": "^3.22.4",
|
"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"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4.1.1",
|
"@tailwindcss/postcss": "^4.1.1",
|
||||||
@@ -98,13 +93,11 @@
|
|||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"postcss": "^8.4.33",
|
"postcss": "^8.4.33",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"prisma": "^6.12.0",
|
"prisma": "^6.5.0",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.8.2",
|
"typescript": "^5.8.2",
|
||||||
"vite": "^6.2.4",
|
"vite": "^6.2.4"
|
||||||
"@types/recordrtc": "^5.6.11",
|
|
||||||
"@types/html2canvas": "^1.0.0"
|
|
||||||
},
|
},
|
||||||
"prisma": {
|
"prisma": {
|
||||||
"seed": "ts-node prisma/seed.ts"
|
"seed": "ts-node prisma/seed.ts"
|
||||||
@@ -120,4 +113,4 @@
|
|||||||
"--window-size=1920x1080"
|
"--window-size=1920x1080"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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");
|
|
||||||
@@ -1067,22 +1067,3 @@ model CalculoPensaoAlimenticia {
|
|||||||
|
|
||||||
@@map("calculos_pensao_alimenticia")
|
@@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
@@ -95,7 +95,6 @@ const router = createBrowserRouter(
|
|||||||
element={<VerificarProva />}
|
element={<VerificarProva />}
|
||||||
errorElement={<ErrorBoundary />}
|
errorElement={<ErrorBoundary />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Rotas para Cálculos Judiciais */}
|
{/* Rotas para Cálculos Judiciais */}
|
||||||
<Route path="/calculos" element={
|
<Route path="/calculos" element={
|
||||||
<ProtectedRoute>
|
<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;
|
export default App;
|
||||||
@@ -1,95 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { DashboardLayout } from './dashboard/DashboardLayout';
|
||||||
|
|
||||||
export const Dashboard: React.FC = () => {
|
export const Dashboard: React.FC = () => {
|
||||||
return (
|
return <DashboardLayout />;
|
||||||
<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>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Dashboard;
|
|
||||||
@@ -116,15 +116,6 @@ export const MainLayout: React.FC = () => {
|
|||||||
icon: MoneyIcon
|
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'] },
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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
@@ -24,10 +24,9 @@ import processosAdministrativosRoutes from './routes/processosAdministrativos';
|
|||||||
import provasRouter from './routes/provas';
|
import provasRouter from './routes/provas';
|
||||||
import calculosJudiciaisRoutes from './routes/calculosJudiciais';
|
import calculosJudiciaisRoutes from './routes/calculosJudiciais';
|
||||||
import indicesMonetariosRoutes from './routes/indicesMonetarios';
|
import indicesMonetariosRoutes from './routes/indicesMonetarios';
|
||||||
import pensaoAlimenticiaRouter from './routes/pensaoAlimenticia';
|
import pensaoAlimenticiaRouter from './routes/pensaoAlimenticia'; // Importar router de pensão alimentícia
|
||||||
import financeiroPainelRoutes from './routes/financeiroPainel';
|
import financeiroPainelRoutes from './routes/financeiroPainel'; // Importar rotas do financeiro painel
|
||||||
import audienciasRoutes from './routes/audiencias';
|
import audienciasRoutes from './routes/audiencias'; // Importar rotas de audiências
|
||||||
import sessionsRouter from './routes/sessions'; // Adicionar importação das rotas de sessão
|
|
||||||
|
|
||||||
|
|
||||||
const app = express();
|
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/anexos', anexosRoutes);
|
||||||
app.use('/api/atendimentos', atendimentosRoutes);
|
app.use('/api/atendimentos', atendimentosRoutes);
|
||||||
app.use('/api/provas', provasRouter);
|
app.use('/api/provas', provasRouter);
|
||||||
app.use('/api/sessions', sessionsRouter); // Adicionar rotas de sessão
|
|
||||||
|
|
||||||
app.use('/api/tarefas', tarefasRoutes);
|
app.use('/api/tarefas', tarefasRoutes);
|
||||||
app.use('/api/dashboard', authMiddleware, dashboardRoutes);
|
app.use('/api/dashboard', authMiddleware, dashboardRoutes);
|
||||||
@@ -1482,5 +1480,5 @@ app.use('/api/processos-administrativos', processosAdministrativosRoutes);
|
|||||||
app.use('/api/calculos', calculosJudiciaisRoutes);
|
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);
|
app.use('/api/financeiro-painel', financeiroPainelRoutes); // Adicionar uso das rotas do financeiro painel
|
||||||
app.use('/api/audiencias', audienciasRoutes);
|
app.use('/api/audiencias', audienciasRoutes); // Adicionar uso das rotas de audiências
|
||||||
@@ -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;
|
|
||||||
@@ -4,7 +4,6 @@ import { createHash } from 'crypto';
|
|||||||
import { PDFDocument, StandardFonts } from 'pdf-lib';
|
import { PDFDocument, StandardFonts } from 'pdf-lib';
|
||||||
import * as OpenTimestamps from 'opentimestamps';
|
import * as OpenTimestamps from 'opentimestamps';
|
||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
import { promises as fs } from 'fs';
|
|
||||||
|
|
||||||
export class ProvaService {
|
export class ProvaService {
|
||||||
private fileValidator: FileValidator;
|
private fileValidator: FileValidator;
|
||||||
@@ -97,51 +96,4 @@ export class ProvaService {
|
|||||||
buffer: await pdfDoc.save()
|
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}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in new issue
Block a user