frontend; backend: adiciona sistema novo de gravacao de provas
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
This commit is contained in:
1 parent
0750b26208
commit
5a00eea21f
13 files changed
+1466
-8
No files matched your search
@@ -0,0 +1,361 @@
|
||||
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