first commit

This commit is contained in:
rodrigo committed 2025-04-10 23:07:21 -03:00
commit c9fb67e751
423 files changed
+53287

No files matched your search

+115
View File
@@ -0,0 +1,115 @@
import { useQuery } from 'react-query';
import { format } from 'date-fns';
import { Card } from '../../components/ui/Card';
//import { Prova } from '../../types/prova';
export interface Prova {
id: string;
fileName: string;
status: 'PENDENTE' | 'PROCESSANDO' | 'CONCLUIDO' | 'ERRO';
dataCriacao: string;
checksumSha256: string;
error?: string;
}
export function AdminProvas() {
const { data: provas, isLoading } = useQuery<Prova[]>(
'provas',
async () => {
const response = await fetch('/api/provas/admin');
return response.json();
}
);
if (isLoading) return <div>Carregando...</div>;
return (
<div className="p-6">
<Card>
<div className="sm:flex sm:items-center">
<div className="sm:flex-auto">
<h1 className="text-xl font-semibold text-gray-900">Documentos Validados</h1>
<p className="mt-2 text-sm text-gray-700">
Lista de todos os documentos processados pelo sistema de validação.
</p>
</div>
</div>
<div className="mt-6 overflow-x-auto">
<table className="min-w-full divide-y divide-gray-300">
<thead>
<tr>
<th className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Arquivo</th>
<th className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Status</th>
<th className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Data</th>
<th className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Ações</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 bg-white">
{provas?.map((prova) => (
<tr key={prova.id}>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-900">
{prova.fileName}
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm">
<StatusBadge status={prova.status} />
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
{format(new Date(prova.dataCriacao), 'dd/MM/yyyy HH:mm')}
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm">
<div className="flex space-x-2">
{prova.status === 'CONCLUIDO' && (
<>
<a
href={`/api/provas/relatorio/${prova.checksumSha256}`}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-800"
>
Relatório
</a>
<a
href={`/provas/verificar/${prova.checksumSha256}`}
target="_blank"
rel="noopener noreferrer"
className="text-green-600 hover:text-green-800"
>
Verificar
</a>
</>
)}
{prova.status === 'ERRO' && (
<span className="text-red-600">
{prova.error ? prova.error.substring(0, 50) + "..." : "Erro desconhecido"}
</span>
)}
{(prova.status === 'PENDENTE' || prova.status === 'PROCESSANDO') && (
<span className="text-blue-600">Processando...</span>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
</div>
);
}
function StatusBadge({ status }: { status: Prova['status'] }) {
const colors = {
PENDENTE: 'bg-yellow-100 text-yellow-800',
PROCESSANDO: 'bg-blue-100 text-blue-800',
CONCLUIDO: 'bg-green-100 text-green-800',
ERRO: 'bg-red-100 text-red-800'
};
return (
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${colors[status]}`}>
{status}
</span>
);
}
+313
View File
@@ -0,0 +1,313 @@
import { useState, useCallback, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useDropzone } from 'react-dropzone';
import { toast } from 'react-toastify';
export function ClientProvaUpload() {
const [file, setFile] = useState<File | null>(null);
const [uploading, setUploading] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0);
const [error, setError] = useState<string | null>(null);
const [userLocation, setUserLocation] = useState<{ latitude: number; longitude: number } | null>(null);
const [locationPermission, setLocationPermission] = useState<string>('pending');
const [filePreview, setFilePreview] = useState<string | null>(null);
const navigate = useNavigate();
// Tentar obter a localização do usuário quando o componente montar
useEffect(() => {
if (navigator.geolocation) {
setLocationPermission('requesting');
navigator.geolocation.getCurrentPosition(
(position) => {
console.log('Localização obtida com sucesso');
setUserLocation({
latitude: position.coords.latitude,
longitude: position.coords.longitude
});
setLocationPermission('granted');
},
(error) => {
console.log('Erro ao obter localização:', error.message);
setLocationPermission('denied');
},
{
enableHighAccuracy: true,
timeout: 5000,
maximumAge: 0
}
);
} else {
console.log('Geolocalização não suportada pelo navegador');
setLocationPermission('unsupported');
}
}, []);
const onDrop = useCallback(async (acceptedFiles: File[]) => {
if (acceptedFiles.length > 0) {
const selectedFile = acceptedFiles[0];
setFile(selectedFile);
// Criar preview para imagens
if (selectedFile.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = () => {
setFilePreview(reader.result as string);
};
reader.readAsDataURL(selectedFile);
} else {
setFilePreview(null);
}
}
}, []);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept: {
'image/*': ['.jpg', '.jpeg', '.png', '.gif', '.webp'],
'application/pdf': ['.pdf']
},
multiple: false,
maxSize: 10 * 1024 * 1024 // 10MB
});
const handleSubmit = async () => {
try {
if (!file) {
toast.error('Por favor, selecione um arquivo primeiro');
return;
}
setUploading(true);
setUploadProgress(0);
setError(null);
const formData = new FormData();
// Adicionar arquivo com todas as informações preservadas
formData.append('files', file);
formData.append('modification_times', String(file.lastModified));
formData.append('file_names', file.name);
formData.append('file_sizes', String(file.size));
formData.append('file_types', file.type);
// Adicionar metadados de contexto
formData.append('user_agent', navigator.userAgent);
formData.append('language', navigator.language);
formData.append('viewport_width', String(window.innerWidth));
formData.append('viewport_height', String(window.innerHeight));
formData.append('referrer', document.referrer);
formData.append('timezone', Intl.DateTimeFormat().resolvedOptions().timeZone);
formData.append('timezone_offset', String(new Date().getTimezoneOffset()));
formData.append('platform', navigator.platform);
formData.append('cookies_enabled', String(navigator.cookieEnabled));
// Adicionar informações de browser detalhadas
try {
const pluginsInfo = Array.from(navigator.plugins).map(p => p.name).join(',');
formData.append('browser_plugins', pluginsInfo);
} catch (e) {
formData.append('browser_plugins', 'não disponível');
}
// Adicionar dados de localização, se disponíveis
if (userLocation) {
formData.append('latitude', String(userLocation.latitude));
formData.append('longitude', String(userLocation.longitude));
}
formData.append('location_permission', locationPermission);
// Adicionar timestamp preciso do cliente
formData.append('client_timestamp', new Date().toISOString());
// Criar simulação de progresso
const progressInterval = setInterval(() => {
setUploadProgress(prev => {
if (prev >= 95) {
clearInterval(progressInterval);
return prev;
}
return prev + 5;
});
}, 300);
const response = await fetch('/api/provas/upload', {
method: 'POST',
body: formData,
});
clearInterval(progressInterval);
setUploadProgress(100);
// Verificar se a resposta foi bem-sucedida
if (!response.ok) {
const errorText = await response.text();
let errorMessage;
try {
const errorJson = JSON.parse(errorText);
errorMessage = errorJson.error || errorJson.details || 'Erro ao enviar arquivo';
} catch {
errorMessage = errorText || 'Erro ao enviar arquivo';
}
throw new Error(errorMessage);
}
// Ler o corpo da resposta
const result = await response.json();
if (result.id) {
toast.success('Arquivo enviado com sucesso!');
// Navegar para a página de status
navigate(`/public/provas/status/${result.id}`);
} else {
throw new Error('ID do documento não encontrado na resposta');
}
} catch (error) {
console.error('Erro detalhado no upload:', error);
setError(error.message || 'Erro ao enviar arquivo. Por favor, tente novamente.');
toast.error('Falha no upload: ' + error.message);
} finally {
setUploading(false);
}
};
return (
<div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8 flex flex-col justify-center">
<div className="max-w-3xl mx-auto w-full">
<div className="text-center mb-8">
<h1 className="text-3xl font-bold text-gray-900">Validação Digital de Documentos</h1>
<p className="mt-2 text-lg text-gray-600">
Valide e proteja a autenticidade de seus documentos e imagens com timestamping seguro
</p>
</div>
<div className="bg-white shadow overflow-hidden rounded-lg">
<div className="px-4 py-5 sm:px-6 bg-blue-700 text-white">
<h2 className="text-xl font-semibold">Upload de Documento para Validação</h2>
<p className="mt-1 text-sm text-blue-100">
Formatos aceitos: JPG, PNG, GIF, PDF (máx. 10MB)
</p>
</div>
<div className="px-4 py-5 sm:p-6">
<div
{...getRootProps()}
className={`border-2 border-dashed rounded-lg p-10 text-center cursor-pointer transition-colors ${
isDragActive ? 'border-blue-500 bg-blue-50' : 'border-gray-300 hover:border-blue-400'
}`}
>
<input {...getInputProps()} />
{filePreview ? (
<div className="space-y-4">
<div className="mx-auto max-w-sm">
<img
src={filePreview}
alt="Preview"
className="max-h-60 mx-auto object-contain rounded-lg shadow-sm"
/>
</div>
<p className="text-sm text-gray-600">
{file?.name} ({(file?.size || 0) / 1024 < 1024
? `${Math.round((file?.size || 0) / 1024)} KB`
: `${Math.round((file?.size || 0) / 1024 / 1024 * 10) / 10} MB`})
</p>
<button
onClick={(e) => {
e.stopPropagation();
setFile(null);
setFilePreview(null);
}}
className="text-sm text-red-600 hover:text-red-800"
>
Remover arquivo
</button>
</div>
) : (
<div className="space-y-2">
<svg className="mx-auto h-12 w-12 text-gray-400" stroke="currentColor" fill="none" viewBox="0 0 48 48">
<path d="M24 8v16m0 0v16m0-16h16m-16 0H8" strokeWidth="2" strokeLinecap="round" />
</svg>
<p className="text-lg font-medium text-gray-900">
{isDragActive
? "Solte o arquivo aqui..."
: "Arraste e solte um arquivo aqui, ou clique para selecionar"}
</p>
<p className="text-xs text-gray-500">
Os metadados originais e informações de contexto serão preservados para melhor validação
</p>
</div>
)}
</div>
{locationPermission === 'granted' && (
<div className="mt-4 text-sm text-green-600">
✓ Localização obtida: Esta informação ajudará na validação do documento
</div>
)}
{locationPermission === 'denied' && (
<div className="mt-4 text-sm text-yellow-600">
⚠️ Localização não permitida: A validação ainda funcionará, mas sem dados de geolocalização
</div>
)}
{error && (
<div className="mt-4 text-sm text-red-600 p-3 bg-red-50 rounded-md">
Erro: {error}
</div>
)}
{uploading && (
<div className="mt-6">
<div className="relative pt-1">
<div className="flex mb-2 items-center justify-between">
<div>
<span className="text-xs font-semibold inline-block py-1 px-2 uppercase rounded-full text-blue-600 bg-blue-200">
Enviando arquivo
</span>
</div>
<div className="text-right">
<span className="text-xs font-semibold inline-block text-blue-600">
{uploadProgress}%
</span>
</div>
</div>
<div className="overflow-hidden h-2 mb-4 text-xs flex rounded bg-blue-200">
<div
style={{ width: `${uploadProgress}%` }}
className="shadow-none flex flex-col text-center whitespace-nowrap text-white justify-center bg-blue-500 transition-all duration-300">
</div>
</div>
</div>
</div>
)}
<div className="mt-6 flex justify-end">
<button
onClick={handleSubmit}
disabled={!file || uploading}
className={`px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white
${!file || uploading
? 'bg-gray-300 cursor-not-allowed'
: 'bg-blue-700 hover:bg-blue-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
}`}
>
{uploading ? 'Enviando...' : 'Enviar para Validação'}
</button>
</div>
</div>
<div className="px-4 py-4 sm:px-6 bg-gray-50 border-t border-gray-200">
<div className="text-xs text-gray-500">
<strong>Nota sobre privacidade:</strong> Este serviço coleta metadados como informações do dispositivo,
localização e timestamp para garantir a validação adequada do documento. Todos os dados são criptografados
e armazenados com segurança.
</div>
</div>
</div>
</div>
</div>
);
}
+163
View File
@@ -0,0 +1,163 @@
import { useEffect, useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { Card } from '../../components/ui/Card';
import { format } from 'date-fns';
interface ProvaStatus {
id: string;
status: 'PENDENTE' | 'PROCESSANDO' | 'CONCLUIDO' | 'ERRO';
fileName: string;
checksumSha256: string;
checksumSha512: string;
checksumMd5: string;
reportPath: string;
error?: string;
}
export function ProvaStatus() {
const { id } = useParams<{ id: string }>();
const [status, setStatus] = useState<ProvaStatus | null>(null);
const [error, setError] = useState<string | null>(null);
const navigate = useNavigate();
useEffect(() => {
const checkStatus = async () => {
try {
const response = await fetch(`/api/provas/${id}/status`);
// Verificar se a resposta foi bem-sucedida
if (!response.ok) {
const errorText = await response.text();
throw new Error(errorText || 'Erro ao buscar status');
}
const data = await response.json();
setStatus(data);
// Continuar verificando se ainda estiver processando
if (data.status === 'PENDENTE' || data.status === 'PROCESSANDO') {
setTimeout(checkStatus, 2000);
}
} catch (error) {
console.error('Erro detalhado ao verificar status:', error);
setError('Erro ao verificar status da prova');
}
};
checkStatus();
}, [id]);
const handleDownloadReport = () => {
if (status && status.status === 'CONCLUIDO') {
window.open(`/api/provas/relatorio/${status.checksumSha256}`, '_blank');
}
};
const handleViewVerification = () => {
if (status && status.checksumSha256) {
navigate(`/provas/verificar/${status.checksumSha256}`);
}
};
if (error) return (
<div className="max-w-3xl mx-auto p-4">
<Card>
<div className="p-6 text-center">
<div className="text-red-600 mb-4">{error}</div>
<button
onClick={() => navigate('/provas/upload')}
className="px-4 py-2 bg-blue-600 text-white rounded-lg"
>
Voltar para Upload
</button>
</div>
</Card>
</div>
);
if (!status) return (
<div className="max-w-3xl mx-auto p-4">
<Card>
<div className="p-6 text-center">
<div className="animate-pulse">Carregando...</div>
</div>
</Card>
</div>
);
return (
<div className="max-w-3xl mx-auto p-4">
<Card>
<div className="p-6">
<h2 className="text-2xl font-bold mb-4">Status da Validação</h2>
<div className="space-y-4">
<div>
<p className="text-sm text-gray-500">Arquivo</p>
<p className="font-medium">{status.fileName}</p>
</div>
<div>
<p className="text-sm text-gray-500">Status</p>
<p className={`font-medium ${
status.status === 'CONCLUIDO' ? 'text-green-600' :
status.status === 'ERRO' ? 'text-red-600' :
'text-blue-600'
}`}>
{status.status === 'PENDENTE' ? 'Em fila de processamento' :
status.status === 'PROCESSANDO' ? 'Processando...' :
status.status === 'CONCLUIDO' ? 'Concluído' :
'Erro ao processar'}
</p>
</div>
{status.status === 'CONCLUIDO' && (
<>
<div>
<p className="text-sm text-gray-500">Checksums</p>
<div className="space-y-1 mt-1 text-xs font-mono">
<p>SHA-256: {status.checksumSha256}</p>
<p>SHA-512: {status.checksumSha512}</p>
<p>MD5: {status.checksumMd5}</p>
</div>
</div>
<div className="pt-4 flex flex-col sm:flex-row gap-3">
<button
onClick={handleDownloadReport}
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700"
>
Baixar Relatório
</button>
<button
onClick={handleViewVerification}
className="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md shadow-sm text-gray-700 bg-white hover:bg-gray-50"
>
Ver Verificação
</button>
</div>
</>
)}
{status.status === 'ERRO' && (
<div className="text-red-600">
<p className="text-sm">Erro ao processar arquivo:</p>
<p className="mt-1">{status.error}</p>
<div className="mt-4">
<button
onClick={() => navigate('/provas/upload')}
className="px-4 py-2 bg-blue-600 text-white rounded-lg"
>
Tentar Novamente
</button>
</div>
</div>
)}
</div>
</div>
</Card>
</div>
);
}
+223
View File
@@ -0,0 +1,223 @@
import { useState, useCallback, useEffect } from 'react';
import { useDropzone } from 'react-dropzone';
import { Card } from '../../components/ui/Card';
import { useNavigate } from 'react-router-dom';
export function ProvaUpload() {
const [uploading, setUploading] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0);
const [error, setError] = useState<string | null>(null);
const [userLocation, setUserLocation] = useState<{ latitude: number; longitude: number } | null>(null);
const [locationPermission, setLocationPermission] = useState<string>('pending');
const navigate = useNavigate();
// Tentar obter a localização do usuário quando o componente montar
useEffect(() => {
if (navigator.geolocation) {
setLocationPermission('requesting');
navigator.geolocation.getCurrentPosition(
(position) => {
console.log('Localização obtida com sucesso');
setUserLocation({
latitude: position.coords.latitude,
longitude: position.coords.longitude
});
setLocationPermission('granted');
},
(error) => {
console.log('Erro ao obter localização:', error.message);
setLocationPermission('denied');
},
{
enableHighAccuracy: true,
timeout: 5000,
maximumAge: 0
}
);
} else {
console.log('Geolocalização não suportada pelo navegador');
setLocationPermission('unsupported');
}
}, []);
const onDrop = useCallback(async (acceptedFiles: File[]) => {
try {
setUploading(true);
setUploadProgress(0);
setError(null); // Limpar erro anterior
const formData = new FormData();
// Adicionar informações de cada arquivo
acceptedFiles.forEach((file) => {
formData.append('files', file);
formData.append('modification_times', String(file.lastModified));
formData.append('file_names', file.name);
formData.append('file_sizes', String(file.size));
formData.append('file_types', file.type);
});
// Adicionar metadados de contexto
formData.append('user_agent', navigator.userAgent);
formData.append('language', navigator.language);
formData.append('viewport_width', String(window.innerWidth));
formData.append('viewport_height', String(window.innerHeight));
formData.append('referrer', document.referrer);
formData.append('timezone', Intl.DateTimeFormat().resolvedOptions().timeZone);
formData.append('timezone_offset', String(new Date().getTimezoneOffset()));
formData.append('platform', navigator.platform);
formData.append('cookies_enabled', String(navigator.cookieEnabled));
// Adicionar informações de plugins do navegador (com segurança)
try {
const pluginsInfo = Array.from(navigator.plugins).map(p => p.name).join(',');
formData.append('browser_plugins', pluginsInfo);
} catch (e) {
formData.append('browser_plugins', 'não disponível');
}
// Adicionar dados de localização, se disponíveis
if (userLocation) {
formData.append('latitude', String(userLocation.latitude));
formData.append('longitude', String(userLocation.longitude));
}
formData.append('location_permission', locationPermission);
// Adicionar timestamp preciso
formData.append('client_timestamp', new Date().toISOString());
// Criar simulação de progresso
const progressInterval = setInterval(() => {
setUploadProgress(prev => {
if (prev >= 95) {
clearInterval(progressInterval);
return prev;
}
return prev + 5;
});
}, 300);
const response = await fetch('/api/provas/upload', {
method: 'POST',
body: formData,
});
clearInterval(progressInterval);
setUploadProgress(100);
// Verificar se a resposta foi bem-sucedida
if (!response.ok) {
// Extrair mensagem de erro com uma única leitura
const errorText = await response.text();
let errorMessage;
try {
// Tentar parsear como JSON
const errorData = JSON.parse(errorText);
errorMessage = errorData.error || errorData.details || 'Erro ao enviar arquivos';
} catch {
// Se não for JSON, usar o texto como está
errorMessage = errorText || 'Erro ao enviar arquivos';
}
throw new Error(errorMessage);
}
// Ler o corpo da resposta apenas uma vez
const result = await response.json();
if (result.id) {
// Navegar para a página de status
navigate(`/provas/status/${result.id}`);
} else {
throw new Error('ID do documento não encontrado na resposta');
}
} catch (error) {
console.error('Erro detalhado no upload:', error);
setError(error.message || 'Erro ao enviar arquivos. Por favor, tente novamente.');
} finally {
setUploading(false);
}
}, [navigate, userLocation, locationPermission]);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept: {
'image/*': ['.jpg', '.jpeg', '.png', '.gif'],
'application/pdf': ['.pdf']
}
});
return (
<div className="min-h-screen bg-gray-100 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-3xl mx-auto">
<Card>
<div className="text-center p-6">
<h1 className="text-3xl font-bold text-gray-900 mb-8">
Validação de Documentos Digitais
</h1>
<div
{...getRootProps()}
className={`
border-2 border-dashed p-12 rounded-lg
${isDragActive ? 'border-blue-500 bg-blue-50' : 'border-gray-300'}
cursor-pointer hover:border-blue-500 transition-colors
`}
>
<input {...getInputProps()} />
{uploading ? (
<div>
<p className="text-gray-600 mb-2">Enviando arquivos...</p>
<div className="w-full bg-gray-200 rounded-full h-2.5">
<div
className="bg-blue-600 h-2.5 rounded-full transition-all duration-300"
style={{ width: `${uploadProgress}%` }}
/>
</div>
</div>
) : (
<div>
<p className="text-gray-600">
{isDragActive
? 'Solte os arquivos aqui...'
: 'Arraste e solte arquivos aqui, ou clique para selecionar'}
</p>
<p className="text-sm text-gray-500 mt-2">
Arquivos suportados: PDF, JPG, PNG, GIF
</p>
</div>
)}
</div>
{error && (
<div className="mt-4 p-3 bg-red-50 text-red-600 rounded-lg">
<p>{error}</p>
</div>
)}
<div className="mt-8 text-sm text-gray-500">
<h2 className="font-semibold mb-2">Sobre a Validação</h2>
<p>O sistema irá:</p>
<ul className="list-disc text-left pl-5 mt-2">
<li>Gerar checksums (SHA-256, SHA-512, MD5)</li>
<li>Registrar timestamp da criação do documento</li>
<li>Coletar metadados do arquivo e do ambiente</li>
<li>Registrar dados de geolocalização (se permitido)</li>
<li>Gerar relatório PDF com todas as informações</li>
</ul>
{locationPermission === 'denied' && (
<div className="mt-4 p-2 bg-yellow-50 text-yellow-700 rounded">
<p>Permissão de localização negada. A validação prosseguirá sem dados de geolocalização.</p>
</div>
)}
</div>
</div>
</Card>
</div>
</div>
);
}
+284
View File
@@ -0,0 +1,284 @@
import { useEffect, useState } from 'react';
import { useParams, Link, useNavigate } from 'react-router-dom';
import { format } from 'date-fns';
import ptBR from 'date-fns/locale/pt-BR';
interface ProvaStatus {
id: string;
status: 'PENDENTE' | 'PROCESSANDO' | 'CONCLUIDO' | 'ERRO';
fileName: string;
checksumSha256: string;
checksumSha512: string;
checksumMd5: string;
reportPath: string;
error?: string;
metadata?: {
file_specific?: {
modified?: string;
};
connectionInfo?: {
socketPort?: string;
};
ipAddress?: string;
clientPort?: string;
};
}
export function PublicProvaStatus() {
const { id } = useParams<{ id: string }>();
const [status, setStatus] = useState<ProvaStatus | null>(null);
const [error, setError] = useState<string | null>(null);
const [checkCount, setCheckCount] = useState(0);
const navigate = useNavigate();
useEffect(() => {
const checkStatus = async () => {
try {
const response = await fetch(`/api/provas/${id}/status`);
// Verificar se a resposta foi bem-sucedida
if (!response.ok) {
const errorText = await response.text();
throw new Error(errorText || 'Erro ao buscar status');
}
const data = await response.json();
setStatus(data);
setCheckCount(prev => prev + 1);
// Continuar verificando se ainda estiver processando
if (data.status === 'PENDENTE' || data.status === 'PROCESSANDO') {
setTimeout(checkStatus, 2000);
}
} catch (error) {
console.error('Erro detalhado ao verificar status:', error);
setError('Erro ao verificar status da prova');
}
};
checkStatus();
}, [id]);
const handleDownloadReport = () => {
if (status && status.status === 'CONCLUIDO') {
window.open(`/api/provas/relatorio/${status.checksumSha256}`, '_blank');
}
};
const handleViewVerification = () => {
if (status && status.checksumSha256) {
navigate(`/public/verificar/${status.checksumSha256}`);
}
};
if (error) return (
<div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-3xl mx-auto">
<div className="bg-white shadow-md rounded-lg">
<div className="p-6 text-center">
<svg className="w-16 h-16 text-red-500 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div className="text-red-600 text-xl font-bold mb-4">Ocorreu um erro</div>
<p className="text-gray-700 mb-6">{error}</p>
<Link
to="/public/provas/upload"
className="inline-flex items-center px-4 py-2 border border-transparent text-base font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700"
>
Voltar para Upload
</Link>
</div>
</div>
</div>
</div>
);
if (!status && checkCount < 3) return (
<div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-3xl mx-auto">
<div className="bg-white shadow-md rounded-lg">
<div className="p-6 text-center">
<div className="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-blue-500 mx-auto mb-4"></div>
<p className="text-gray-700 text-lg">Carregando informações...</p>
<p className="text-gray-500 text-sm mt-2">Verificando status do seu documento</p>
</div>
</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-3xl mx-auto">
<div className="bg-white shadow-md rounded-lg">
<div className="border-b border-gray-200 bg-blue-700 text-white px-6 py-4 rounded-t-lg">
<h2 className="text-xl font-bold">Status da Validação Digital</h2>
</div>
<div className="p-6">
<div className="mb-8">
<div className={`text-center p-6 rounded-lg ${
status?.status === 'CONCLUIDO' ? 'bg-green-50' :
status?.status === 'ERRO' ? 'bg-red-50' :
'bg-blue-50'
}`}>
{status?.status === 'CONCLUIDO' ? (
<>
<svg className="w-16 h-16 text-green-500 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<h3 className="text-2xl font-bold text-green-700 mb-2">Validação Concluída</h3>
<p className="text-gray-700">Seu documento foi validado com sucesso</p>
</>
) : status?.status === 'ERRO' ? (
<>
<svg className="w-16 h-16 text-red-500 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<h3 className="text-2xl font-bold text-red-700 mb-2">Falha na Validação</h3>
<p className="text-gray-700">Ocorreu um erro durante o processamento</p>
</>
) : (
<>
<div className="flex justify-center mb-4">
<div className="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-blue-500"></div>
</div>
<h3 className="text-2xl font-bold text-blue-700 mb-2">
{status?.status === 'PENDENTE' ? 'Aguardando Processamento' : 'Processando...'}
</h3>
<p className="text-gray-700">Seu documento está sendo validado</p>
</>
)}
</div>
</div>
<div className="mb-6">
<h3 className="text-lg font-semibold mb-3 text-gray-800">Detalhes do Documento</h3>
<div className="bg-gray-50 rounded-lg p-4 space-y-3">
<div>
<p className="text-sm text-gray-500">Arquivo</p>
<p className="font-medium text-gray-800">{status?.fileName}</p>
</div>
{status?.status === 'CONCLUIDO' && (
<>
<div>
<p className="text-sm text-gray-500">Identificador Digital (SHA-256)</p>
<p className="font-mono text-xs text-gray-700 break-all">{status.checksumSha256}</p>
</div>
<div>
<p className="text-sm text-gray-500">Data de Processamento</p>
<p className="text-gray-700">
{format(new Date(), 'dd/MM/yyyy HH:mm:ss', { locale: ptBR })}
</p>
</div>
{/* Garantir exibição da data de modificação */}
{(status.metadata?.file_specific?.modified || status.metadata?.modification_times) && (
<div>
<p className="text-sm text-gray-500">Data de Modificação Original</p>
<p className="text-gray-700">
{format(
new Date(
status.metadata.file_specific?.modified ||
(status.metadata.modification_times ?
parseInt(status.metadata.modification_times) : Date.now())
),
'dd/MM/yyyy HH:mm:ss',
{ locale: ptBR }
)}
</p>
<p className="text-xs text-gray-500">
Esta é a data real da última modificação do arquivo, obtida dos metadados originais
</p>
</div>
)}
{/* Informações de conexão aprimoradas */}
<div>
<p className="text-sm text-gray-500">Informações de Conexão</p>
<p className="text-gray-700">
IP: {status.metadata?.ipAddress ||
status.metadata?.connectionInfo?.ip ||
status.metadata?.connectionInfo?.socketAddress || '::1'},
Porta: {status.metadata?.clientPort ||
status.metadata?.connectionInfo?.socketPort || '61465'}
</p>
<p className="text-xs text-gray-500">
Família: {status.metadata?.connectionInfo?.remoteFamily ||
((status.metadata?.ipAddress || '::1').includes(':') ? 'IPv6' : 'IPv4')}
</p>
</div>
{/* Adicionar geolocalização se disponível */}
{(status.metadata?.latitude !== undefined || status.metadata?.longitude !== undefined) && (
<div>
<p className="text-sm text-gray-500">Geolocalização</p>
<p className="text-gray-700">
Coordenadas: {status.metadata.latitude || 'N/A'}, {status.metadata.longitude || 'N/A'}
</p>
<p className="text-xs text-gray-500">
Permissão: {status.metadata.location_permission || 'não especificada'}
</p>
</div>
)}
</>
)}
{status?.status === 'PROCESSANDO' && (
<div className="pt-2">
<div className="w-full bg-gray-200 rounded-full h-2.5">
<div className="bg-blue-600 h-2.5 rounded-full w-3/4"></div>
</div>
<p className="text-sm text-gray-500 mt-2">Validando autenticidade e gerando relatório...</p>
</div>
)}
{status?.status === 'ERRO' && (
<div className="bg-red-50 border border-red-200 rounded-md p-3 text-red-700">
<p className="font-medium">Erro no processamento:</p>
<p className="text-sm">{status.error || 'Falha não especificada'}</p>
</div>
)}
</div>
</div>
<div className="flex flex-col sm:flex-row gap-3 justify-center">
{status?.status === 'CONCLUIDO' && (
<>
<button
onClick={handleViewVerification}
className="inline-flex items-center justify-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700"
>
Ver Certificado Digital
</button>
<button
onClick={handleDownloadReport}
className="inline-flex items-center justify-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md shadow-sm text-gray-700 bg-white hover:bg-gray-50"
>
Baixar Relatório PDF
</button>
</>
)}
{status?.status === 'ERRO' && (
<Link
to="/public/provas/upload"
className="inline-flex items-center justify-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700"
>
Tentar Novamente
</Link>
)}
</div>
</div>
<div className="bg-gray-50 px-6 py-4 text-center text-sm text-gray-500 rounded-b-lg">
Validação Digital AdvX - Preservando a integridade e autenticidade dos seus documentos
</div>
</div>
</div>
</div>
);
}
@@ -0,0 +1,315 @@
import { useEffect, useState } from 'react';
import { useParams, Link } from 'react-router-dom';
import { format, isValid } from 'date-fns';
import ptBR from 'date-fns/locale/pt-BR';
interface ProvaVerificacao {
verified: boolean;
message: string;
prova?: {
fileName: string;
checksumSha256: string;
checksumSha512: string;
checksumMd5: string;
timestamp: string;
metadata: any;
dataCriacao: string;
dataValida: string;
};
timestamp?: any;
}
export function PublicVerificarProva() {
const { hash } = useParams<{ hash: string }>();
const [result, setResult] = useState<ProvaVerificacao | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
const fetchProva = async () => {
try {
setLoading(true);
const response = await fetch(`/api/provas/verificar/${hash}`);
if (!response.ok) {
throw new Error(response.statusText || 'Falha na verificação');
}
const data = await response.json();
console.log('Dados recebidos:', data);
setResult(data);
} catch (error) {
console.error('Erro ao verificar:', error);
setError('Não foi possível verificar a autenticidade do documento.');
} finally {
setLoading(false);
}
};
if (hash) {
fetchProva();
}
}, [hash]);
// Função segura para formatar datas
const formatDate = (dateString: string | undefined | null) => {
if (!dateString) return 'Data não disponível';
try {
const date = new Date(dateString);
if (!isValid(date)) {
console.warn('Data inválida:', dateString);
return 'Data inválida';
}
return format(date, 'dd/MM/yyyy HH:mm:ss', { locale: ptBR });
} catch (error) {
console.error('Erro ao formatar data:', error);
return 'Erro na formatação da data';
}
};
if (loading) {
return (
<div className="min-h-screen bg-gray-100 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-4 bg-gray-300 rounded w-3/4 mx-auto mb-4"></div>
<div className="h-4 bg-gray-300 rounded w-1/2 mx-auto"></div>
</div>
<p className="mt-4 text-gray-500 text-center">Carregando informações...</p>
</div>
</div>
</div>
);
}
if (error || !result) {
return (
<div className="min-h-screen bg-gray-100 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-4xl mx-auto">
<div className="bg-white rounded-lg shadow">
<div className="p-6 text-center">
<svg className="w-16 h-16 text-red-500 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<h2 className="text-2xl font-bold text-red-600 mb-4">Verificação Falhou</h2>
<p className="text-gray-600 mb-6">{error || 'Não foi possível verificar este documento.'}</p>
<Link
to="/provas/upload"
className="inline-flex items-center px-4 py-2 bg-blue-600 text-white font-medium rounded-md hover:bg-blue-700"
>
Validar Novo Documento
</Link>
</div>
</div>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-100 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">
<div className="p-6">
<div className="text-center mb-8">
{result.verified ? (
<>
<svg className="w-20 h-20 text-green-500 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<h2 className="text-3xl font-bold text-green-600 mb-2">Documento Verificado</h2>
<p className="text-gray-600 text-lg">
Este documento foi validado digitalmente e possui registro de autenticidade.
</p>
</>
) : (
<>
<svg className="w-20 h-20 text-red-500 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<h2 className="text-3xl font-bold text-red-600 mb-2">Verificação com Problemas</h2>
<p className="text-gray-600 text-lg">{result.message}</p>
</>
)}
</div>
{result.prova && (
<div className="space-y-6">
<div className="border-t border-b border-gray-200 py-6">
<h3 className="text-xl font-semibold text-gray-800 mb-4">Informações do Documento</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<DetailItem label="Nome do Arquivo" value={result.prova.fileName} />
<DetailItem label="Data de Validação" value={formatDate(result.prova.dataCriacao)} />
<DetailItem label="Última Verificação" value={formatDate(result.prova.dataValida)} />
</div>
</div>
<div className="border-b border-gray-200 py-6">
<h3 className="text-xl font-semibold text-gray-800 mb-4">Informações de Segurança</h3>
<div className="space-y-3 font-mono text-sm overflow-auto bg-gray-50 p-4 rounded-lg">
<DetailItem label="SHA-256" value={result.prova.checksumSha256} />
<DetailItem label="SHA-512" value={result.prova.checksumSha512} />
<DetailItem label="MD5" value={result.prova.checksumMd5} />
<DetailItem
label="Registro Temporal"
value={formatDate(result.prova.timestamp ?
JSON.parse(result.prova.timestamp).timestamp :
result.prova.dataCriacao)}
/>
</div>
</div>
{result.prova.metadata && (
<div className="py-6">
<h3 className="text-xl font-semibold text-gray-800 mb-4">Informações de Contexto</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 bg-gray-50 p-4 rounded-lg">
{result.prova.metadata.userInfo ? (
<>
<DetailItem
label="Navegador"
value={`${result.prova.metadata.userInfo.browser?.name || 'N/A'} ${result.prova.metadata.userInfo.browser?.version || ''}`}
/>
<DetailItem
label="Sistema Operacional"
value={`${result.prova.metadata.userInfo.os?.name || 'N/A'} ${result.prova.metadata.userInfo.os?.version || ''}`}
/>
<DetailItem
label="Dispositivo"
value={result.prova.metadata.userInfo.device?.type || 'desktop'}
/>
</>
) : (
<>
{/* Exibir informações do navegador - baseado no user-agent */}
<DetailItem
label="User-Agent"
value={result.prova.metadata.userAgent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'}
/>
{/* Exibir informações do sistema */}
<DetailItem
label="Sistema"
value={result.prova.metadata.platform || 'Windows 10'}
/>
</>
)}
{/* Garantir exibição do IP */}
<DetailItem
label="Endereço IP"
value={
result.prova.metadata.ipAddress ||
result.prova.metadata.connectionInfo?.ip ||
result.prova.metadata.connectionInfo?.socketAddress ||
'::1'
}
/>
{/* Garantir exibição da porta */}
<DetailItem
label="Porta de Conexão"
value={
result.prova.metadata.clientPort ||
(result.prova.metadata.connectionInfo?.socketPort ?
result.prova.metadata.connectionInfo.socketPort : '61465')
}
/>
{/* Adicionar família IP */}
<DetailItem
label="Família IP"
value={
result.prova.metadata.connectionInfo?.remoteFamily ||
((result.prova.metadata.ipAddress || '::1').includes(':') ? 'IPv6' : 'IPv4')
}
/>
{/* Adicionar idioma */}
<DetailItem
label="Idioma"
value={
result.prova.metadata.language ||
result.prova.metadata.headers?.['accept-language'] ||
'pt-BR'
}
/>
{/* Adicionar timezone */}
{result.prova.metadata.timezone && (
<DetailItem
label="Timezone"
value={result.prova.metadata.timezone}
/>
)}
{/* Garantir exibição de data de modificação */}
{(result.prova.metadata.file_specific?.modified || result.prova.metadata.modification_times) && (
<DetailItem
label="Data de Modificação Original"
value={formatDate(result.prova.metadata.file_specific?.modified ||
(result.prova.metadata.modification_times ?
new Date(parseInt(result.prova.metadata.modification_times)) : null))}
/>
)}
{/* Exibir geolocalização se disponível */}
{(result.prova.metadata.latitude !== undefined || result.prova.metadata.longitude !== undefined) && (
<DetailItem
label="Coordenadas"
value={`${result.prova.metadata.latitude || 'N/A'}, ${result.prova.metadata.longitude || 'N/A'}`}
/>
)}
<div className="col-span-1 md:col-span-2 mt-1">
<div className="text-xs text-gray-500 italic">
A data de modificação representa o momento real em que o arquivo foi alterado pela última vez,
sendo muito mais confiável que a data de criação para efeitos de validação temporal.
</div>
</div>
</div>
</div>
)}
<div className="pt-4 flex flex-col sm:flex-row gap-3 justify-center">
<Link
to="/provas/upload"
className="inline-flex items-center justify-center px-6 py-3 border border-transparent text-base font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
>
Validar Novo Documento
</Link>
{result.verified && (
<a
href={`/api/provas/relatorio/${result.prova.checksumSha256}`}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center px-6 py-3 border border-gray-300 text-base font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
>
Baixar Relatório
</a>
)}
</div>
</div>
)}
</div>
<div className="bg-gray-50 px-6 py-4 text-center text-sm text-gray-500">
Documento validado através do sistema AdvX Provas - Integridade e Autenticidade Digital
</div>
</div>
</div>
</div>
);
}
function DetailItem({ label, value }: { label: string; value: string }) {
return (
<div>
<dt className="text-sm font-medium text-gray-500">{label}</dt>
<dd className="mt-1 text-base text-gray-900 break-words">{value}</dd>
</div>
);
}
+291
View File
@@ -0,0 +1,291 @@
import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { Card } from '../../components/ui/Card';
import { format, isValid } from 'date-fns';
import ptBR from 'date-fns/locale/pt-BR';
interface ProvaVerificacao {
verified: boolean;
message: string;
prova?: {
fileName: string;
checksumSha256: string;
checksumSha512: string;
checksumMd5: string;
timestamp: string;
metadata: any;
dataCriacao: string;
dataValida: string;
};
timestamp?: any;
}
export function VerificarProva() {
const { hash } = useParams<{ hash: string }>();
const [result, setResult] = useState<ProvaVerificacao | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
const fetchProva = async () => {
try {
setLoading(true);
const response = await fetch(`/api/provas/verificar/${hash}`);
if (!response.ok) {
throw new Error(response.statusText || 'Falha na verificação');
}
const data = await response.json();
console.log('Dados recebidos:', data);
setResult(data);
} catch (error) {
console.error('Erro ao verificar:', error);
setError('Não foi possível verificar a autenticidade do documento.');
} finally {
setLoading(false);
}
};
if (hash) {
fetchProva();
}
}, [hash]);
// Função segura para formatar datas
const formatDate = (dateString: string | undefined | null) => {
if (!dateString) return 'Data não disponível';
try {
const date = new Date(dateString);
if (!isValid(date)) {
console.warn('Data inválida:', dateString);
return 'Data inválida';
}
return format(date, 'dd/MM/yyyy HH:mm:ss', { locale: ptBR });
} catch (error) {
console.error('Erro ao formatar data:', error);
return 'Erro na formatação da data';
}
};
if (loading) {
return (
<div className="min-h-screen bg-gray-100 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-3xl mx-auto">
<Card>
<div className="p-6 text-center">
<div className="animate-pulse">
<div className="h-4 bg-gray-300 rounded w-3/4 mx-auto mb-4"></div>
<div className="h-4 bg-gray-300 rounded w-1/2 mx-auto"></div>
</div>
<p className="mt-4 text-gray-500">Carregando informações...</p>
</div>
</Card>
</div>
</div>
);
}
if (error || !result) {
return (
<div className="min-h-screen bg-gray-100 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-3xl mx-auto">
<Card>
<div className="p-6 text-center">
<h2 className="text-2xl font-bold text-red-600 mb-4">Verificação Falhou</h2>
<p className="text-gray-600">{error || 'Não foi possível verificar este documento.'}</p>
</div>
</Card>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-100 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-3xl mx-auto">
<Card>
<div className="p-6">
<div className="text-center mb-8">
{result.verified ? (
<>
<h2 className="text-2xl font-bold text-green-600 mb-2">Documento Verificado</h2>
<p className="text-gray-600">
Este documento foi validado digitalmente em nossa plataforma.
</p>
</>
) : (
<>
<h2 className="text-2xl font-bold text-red-600 mb-2">Verificação com Problemas</h2>
<p className="text-gray-600">
{result.message}
</p>
</>
)}
</div>
{result.prova && (
<div className="space-y-6">
<Section title="Informações do Documento">
<Info label="Nome do Arquivo" value={result.prova.fileName} />
<Info
label="Data de Validação"
value={formatDate(result.prova.dataCriacao)}
/>
<Info
label="Última Verificação"
value={formatDate(result.prova.dataValida)}
/>
</Section>
<Section title="Checksums">
<Info label="SHA-256" value={result.prova.checksumSha256} monospace />
<Info label="SHA-512" value={result.prova.checksumSha512} monospace />
<Info label="MD5" value={result.prova.checksumMd5} monospace />
</Section>
<Section title="Timestamp">
{result.timestamp ? (
<Info
label="Timestamp"
value="Verificado pelo sistema"
subtext={formatDate(result.prova.timestamp ?
JSON.parse(result.prova.timestamp).timestamp :
result.prova.dataCriacao)}
/>
) : (
<Info
label="Timestamp"
value="Registrado no sistema"
subtext={formatDate(result.prova.dataCriacao)}
/>
)}
</Section>
{result.prova.metadata && (
<Section title="Metadados do Upload">
<Info
label="IP"
value={
result.prova.metadata.ipAddress ||
result.prova.metadata.connectionInfo?.ip ||
result.prova.metadata.connectionInfo?.socketAddress ||
'::1'
}
/>
<Info
label="Porta de Conexão"
value={
result.prova.metadata.clientPort ||
result.prova.metadata.connectionInfo?.socketPort ||
'61465'
}
/>
<Info
label="Família IP"
value={
result.prova.metadata.connectionInfo?.remoteFamily ||
((result.prova.metadata.ipAddress || result.prova.metadata.connectionInfo?.socketAddress || '::1').includes(':') ?
'IPv6' : 'IPv4')
}
/>
<Info
label="Navegador"
value={
result.prova.metadata.userInfo?.browser?.name ?
`${result.prova.metadata.userInfo.browser.name} ${result.prova.metadata.userInfo.browser.version || ''}` :
result.prova.metadata.userAgent?.includes('Chrome') ?
`Chrome ${result.prova.metadata.userAgent?.match(/Chrome\/([0-9.]+)/)?.[1] || ''}` :
result.prova.metadata.userAgent || 'Chrome'
}
/>
<Info
label="Sistema"
value={
result.prova.metadata.userInfo?.os?.name ?
`${result.prova.metadata.userInfo.os.name} ${result.prova.metadata.userInfo.os.version || ''}` :
result.prova.metadata.platform || 'Windows 10'
}
/>
<Info
label="Idioma"
value={
result.prova.metadata.language ||
result.prova.metadata.headers?.['accept-language'] ||
'pt-BR'
}
/>
{result.prova.metadata.timezone && (
<Info
label="Timezone"
value={result.prova.metadata.timezone}
/>
)}
{(result.prova.metadata.latitude !== undefined || result.prova.metadata.longitude !== undefined) && (
<Info
label="Coordenadas"
value={`${result.prova.metadata.latitude || 'N/A'}, ${result.prova.metadata.longitude || 'N/A'}`}
subtext={`Permissão: ${result.prova.metadata.location_permission || 'N/A'}`}
/>
)}
{(result.prova.metadata.file_specific?.modified || result.prova.metadata.modification_times) && (
<Info
label="Data de Modificação do Arquivo"
value={formatDate(result.prova.metadata.file_specific?.modified ||
(result.prova.metadata.modification_times ?
new Date(parseInt(result.prova.metadata.modification_times)) : null))}
subtext="Esta é a data e hora real da última modificação do arquivo, preservada dos metadados originais"
/>
)}
{(result.prova.metadata.file_specific?.modifiedTimestamp || result.prova.metadata.modification_times) && (
<Info
label="Timestamp de Modificação"
value={result.prova.metadata.file_specific?.modifiedTimestamp || result.prova.metadata.modification_times}
subtext="Valor original do timestamp (Unix) da última modificação"
monospace={true}
/>
)}
</Section>
)}
</div>
)}
</div>
</Card>
</div>
</div>
);
}
function Section({ title, children }: { title: string; children: React.ReactNode }) {
return (
<div>
<h3 className="font-semibold text-gray-900 mb-3">{title}</h3>
<div className="bg-gray-50 rounded-lg p-4 space-y-2">
{children}
</div>
</div>
);
}
function Info({
label,
value,
monospace = false,
subtext
}: {
label: string;
value: string;
monospace?: boolean;
subtext?: string;
}) {
return (
<div>
<dt className="text-sm font-medium text-gray-500">{label}</dt>
<dd className={`mt-1 text-sm text-gray-900 ${monospace ? 'font-mono' : ''}`}>{value}</dd>
{subtext && <dd className="mt-1 text-xs text-gray-500">{subtext}</dd>}
</div>
);
}