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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 }; } }