362 lines
11 KiB
TypeScript
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
|
|
};
|
|
}
|
|
}
|