Files
advx/src/services/SessionProvaService.ts
T
2025-07-27 18:35:46 -03:00

362 lines
11 KiB
TypeScript

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