969 lines
32 KiB
JavaScript
969 lines
32 KiB
JavaScript
const express = require('express');
|
|
const http = require('http');
|
|
const socketIo = require('socket.io');
|
|
const fs = require('fs').promises;
|
|
const path = require('path');
|
|
const cors = require('cors');
|
|
const cookieParser = require('cookie-parser');
|
|
const sharp = require('sharp');
|
|
require('dotenv').config();
|
|
|
|
// Importar módulos
|
|
const imageProcessor = require('./image-processor');
|
|
const { authenticateToken, hashPassword, JWT_SECRET } = require('./auth');
|
|
const jwt = require('jsonwebtoken');
|
|
const db = require('./database');
|
|
const storage = require('./storage');
|
|
const authRoutes = require('./routes/auth');
|
|
const imageRoutes = require('./routes/images');
|
|
const patientsRouter = require('./routes/patients');
|
|
const gtosRouter = require('./routes/gtos');
|
|
const systemRoutes = require('./routes/system');
|
|
const { checkInstallation } = require('./installer/check-installation');
|
|
|
|
// ================================================================
|
|
// CONFIGURAÇÃO DO SERVIDOR
|
|
// ================================================================
|
|
|
|
const app = express();
|
|
const server = http.createServer(app);
|
|
const io = socketIo(server, {
|
|
cors: {
|
|
origin: process.env.ALLOWED_ORIGINS || "*",
|
|
methods: ["GET", "POST"]
|
|
},
|
|
transports: ['websocket', 'polling'],
|
|
allowEIO3: true,
|
|
pingTimeout: 60000,
|
|
pingInterval: 25000,
|
|
connectTimeout: 45000,
|
|
upgradeTimeout: 30000,
|
|
maxHttpBufferSize: 5e7 // 50MB para suportar raios-x grandes
|
|
});
|
|
|
|
const PORT = process.env.PORT || 3000;
|
|
const UPLOAD_DIR = process.env.UPLOAD_DIR || path.join(__dirname, 'uploads');
|
|
const PROCESSED_DIR = process.env.PROCESSED_DIR || path.join(__dirname, 'processed');
|
|
|
|
// ================================================================
|
|
// MIDDLEWARES BÁSICOS
|
|
// ================================================================
|
|
|
|
app.use(cors());
|
|
app.use(express.json({ limit: '50mb' }));
|
|
app.use(express.urlencoded({ extended: true, limit: '50mb' }));
|
|
app.use(cookieParser());
|
|
|
|
// ================================================================
|
|
// ROTAS DA API
|
|
// ================================================================
|
|
app.use('/api/patients', patientsRouter);
|
|
|
|
// ================================================================
|
|
// ROTAS PÚBLICAS (SEMPRE ACESSÍVEIS)
|
|
// ================================================================
|
|
|
|
// Health check
|
|
app.get('/status', (req, res) => {
|
|
res.json({
|
|
status: 'online',
|
|
timestamp: new Date().toISOString(),
|
|
uptime: process.uptime()
|
|
});
|
|
});
|
|
|
|
// Debug endpoint for browser logs
|
|
app.post('/api/debug-log', (req, res) => {
|
|
console.log('=== BROWSER HTML ===');
|
|
console.log(req.body.html || 'No HTML provided');
|
|
console.log('====================');
|
|
res.json({ success: true });
|
|
});
|
|
|
|
// Status de conexões Socket.IO
|
|
app.get('/api/socket/status', (req, res) => {
|
|
const sockets = Array.from(io.sockets.sockets.values());
|
|
const clients = sockets.map(socket => ({
|
|
id: socket.id,
|
|
connected: socket.connected,
|
|
transport: socket.conn.transport.name,
|
|
isIdentified: socket.isIdentified || false,
|
|
handshake: socket.handshake.query,
|
|
remoteAddress: socket.handshake.address
|
|
}));
|
|
|
|
const identifiedClients = connectedClients.filter(c => {
|
|
return sockets.some(s => s.id === c.socketId);
|
|
});
|
|
|
|
res.json({
|
|
totalConnections: sockets.length,
|
|
identifiedClients: identifiedClients.length,
|
|
clients: clients,
|
|
identified: identifiedClients,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
});
|
|
|
|
// Endpoint para forçar identificação de um cliente (debug)
|
|
app.post('/api/socket/request-identify/:socketId', (req, res) => {
|
|
const { socketId } = req.params;
|
|
const socket = io.sockets.sockets.get(socketId);
|
|
|
|
if (!socket) {
|
|
return res.status(404).json({ error: 'Socket não encontrado' });
|
|
}
|
|
|
|
socket.emit('please-identify', {
|
|
message: 'Solicitação manual de identificação'
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Solicitação de identificação enviada',
|
|
socketId
|
|
});
|
|
});
|
|
|
|
// Endpoint para teste de conexão Socket.IO
|
|
app.post('/api/socket/test-connection', async (req, res) => {
|
|
try {
|
|
const sockets = Array.from(io.sockets.sockets.values());
|
|
const totalConnections = sockets.length;
|
|
const identifiedClients = connectedClients.filter(c => {
|
|
return sockets.some(s => s.id === c.socketId);
|
|
});
|
|
|
|
if (identifiedClients.length === 0) {
|
|
return res.json({
|
|
success: true,
|
|
totalConnections: totalConnections,
|
|
identifiedClients: 0,
|
|
testResults: [],
|
|
message: 'Nenhum cliente identificado para testar',
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
}
|
|
|
|
// Enviar evento de teste para todos os clientes identificados
|
|
const testPromises = identifiedClients.map((client) => {
|
|
return new Promise((resolve) => {
|
|
const socket = io.sockets.sockets.get(client.socketId);
|
|
|
|
if (!socket || !socket.connected) {
|
|
resolve({
|
|
socketId: client.socketId,
|
|
name: client.name,
|
|
success: false,
|
|
error: 'Cliente não está conectado'
|
|
});
|
|
return;
|
|
}
|
|
|
|
const testId = `test-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
const testStartTime = Date.now();
|
|
let resolved = false;
|
|
|
|
const timeout = setTimeout(() => {
|
|
if (!resolved) {
|
|
resolved = true;
|
|
resolve({
|
|
socketId: client.socketId,
|
|
name: client.name,
|
|
success: false,
|
|
error: 'Timeout - cliente não respondeu em 5 segundos'
|
|
});
|
|
}
|
|
}, 5000);
|
|
|
|
const ackHandler = (data) => {
|
|
if (!resolved && data.testId === testId) {
|
|
resolved = true;
|
|
clearTimeout(timeout);
|
|
socket.off('test-connection-ack', ackHandler);
|
|
resolve({
|
|
socketId: client.socketId,
|
|
name: client.name,
|
|
success: true,
|
|
latency: data.latency || (Date.now() - testStartTime),
|
|
timestamp: data.timestamp
|
|
});
|
|
}
|
|
};
|
|
|
|
socket.on('test-connection-ack', ackHandler);
|
|
|
|
socket.emit('test-connection', {
|
|
testId: testId,
|
|
timestamp: testStartTime,
|
|
message: 'Teste de conexão iniciado pelo servidor'
|
|
});
|
|
});
|
|
});
|
|
|
|
// Aguardar todas as respostas
|
|
const testResults = await Promise.all(testPromises);
|
|
|
|
res.json({
|
|
success: true,
|
|
totalConnections: totalConnections,
|
|
identifiedClients: identifiedClients.length,
|
|
testResults: testResults,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Erro no teste de conexão:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// API de Instalação - Status
|
|
app.get('/api/install/status', async (req, res) => {
|
|
try {
|
|
const status = await checkInstallation();
|
|
res.json(status);
|
|
} catch (error) {
|
|
res.json({
|
|
installed: false,
|
|
needsInstall: true,
|
|
databaseExists: false,
|
|
userExists: false,
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// API de Instalação - Executar
|
|
app.post('/api/install', async (req, res) => {
|
|
try {
|
|
const { db: dbConfig, admin } = req.body;
|
|
|
|
// Validar dados
|
|
if (!dbConfig || !admin) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Dados incompletos'
|
|
});
|
|
}
|
|
|
|
if (!admin.username || !admin.email || !admin.password) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Preencha todos os dados do administrador'
|
|
});
|
|
}
|
|
|
|
if (admin.password.length < 8) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'A senha deve ter no mínimo 8 caracteres'
|
|
});
|
|
}
|
|
|
|
// Atualizar variáveis de ambiente
|
|
process.env.DB_HOST = dbConfig.host || 'localhost';
|
|
process.env.DB_PORT = dbConfig.port || 3306;
|
|
process.env.DB_NAME = dbConfig.name || 'dental_images';
|
|
process.env.DB_USER = dbConfig.user || 'root';
|
|
process.env.DB_PASSWORD = dbConfig.password || '';
|
|
|
|
// Salvar no arquivo .env
|
|
await updateEnvFile({
|
|
DB_HOST: process.env.DB_HOST,
|
|
DB_PORT: process.env.DB_PORT,
|
|
DB_NAME: process.env.DB_NAME,
|
|
DB_USER: process.env.DB_USER,
|
|
DB_PASSWORD: process.env.DB_PASSWORD
|
|
});
|
|
|
|
// Inicializar banco de dados
|
|
await db.initDatabase();
|
|
console.log('✅ Banco de dados inicializado');
|
|
|
|
// Criar usuário admin
|
|
const passwordHash = await hashPassword(admin.password);
|
|
await db.run(
|
|
`INSERT INTO users (username, email, password_hash, created_at)
|
|
VALUES (?, ?, ?, NOW())
|
|
ON CONFLICT (username) DO UPDATE SET
|
|
email = EXCLUDED.email,
|
|
password_hash = EXCLUDED.password_hash`,
|
|
[admin.username, admin.email, passwordHash]
|
|
);
|
|
console.log('✅ Usuário admin criado:', admin.username);
|
|
|
|
// Criar arquivo .installed
|
|
const installedPath = path.join(__dirname, '.installed');
|
|
await fs.writeFile(installedPath, new Date().toISOString());
|
|
|
|
// Criar arquivo version.txt
|
|
const pkg = require('./package.json');
|
|
const versionPath = path.join(__dirname, 'version.txt');
|
|
await fs.writeFile(versionPath, pkg.version || '2.0.0');
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Instalação concluída com sucesso!'
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Erro na instalação:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: error.message || 'Erro na instalação'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Função para atualizar arquivo .env
|
|
async function updateEnvFile(updates) {
|
|
const envPath = path.join(__dirname, '.env');
|
|
let envContent = '';
|
|
|
|
try {
|
|
// Ler arquivo .env existente
|
|
envContent = await fs.readFile(envPath, 'utf8');
|
|
} catch (error) {
|
|
// Se não existe, criar conteúdo inicial
|
|
envContent = `PORT=3000
|
|
JWT_SECRET=${process.env.JWT_SECRET || 'sua-chave-secreta-alterar-em-producao'}
|
|
SESSION_SECRET=${process.env.SESSION_SECRET || 'outra-chave-secreta'}
|
|
ALLOWED_ORIGINS=${process.env.ALLOWED_ORIGINS || '*'}
|
|
TRUST_PROXY=true
|
|
|
|
`;
|
|
}
|
|
|
|
// Atualizar valores
|
|
Object.keys(updates).forEach(key => {
|
|
const regex = new RegExp(`^${key}=.*$`, 'm');
|
|
const newLine = `${key}=${updates[key]}`;
|
|
|
|
if (regex.test(envContent)) {
|
|
envContent = envContent.replace(regex, newLine);
|
|
} else {
|
|
envContent += `${newLine}\n`;
|
|
}
|
|
});
|
|
|
|
// Adicionar configurações de diretórios se não existirem
|
|
if (!envContent.includes('UPLOAD_DIR')) {
|
|
envContent += `UPLOAD_DIR=${UPLOAD_DIR}\n`;
|
|
}
|
|
if (!envContent.includes('PROCESSED_DIR')) {
|
|
envContent += `PROCESSED_DIR=${PROCESSED_DIR}\n`;
|
|
}
|
|
|
|
// Salvar arquivo
|
|
await fs.writeFile(envPath, envContent);
|
|
console.log('✅ Arquivo .env atualizado');
|
|
}
|
|
|
|
// Página de instalação
|
|
app.get('/install', (req, res) => {
|
|
res.sendFile(path.join(__dirname, 'public', 'install.html'));
|
|
});
|
|
|
|
// ================================================================
|
|
// ROTA PRINCIPAL (Página web) - PRIMEIRA ROTA A SER DEFINIDA
|
|
// ================================================================
|
|
|
|
// ROTA PRINCIPAL (Página web)
|
|
app.get('/', (req, res) => {
|
|
res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
|
});
|
|
|
|
// Favicon redirect/fallback
|
|
app.get('/favicon.ico', (req, res) => {
|
|
res.sendFile(path.join(__dirname, 'public', 'favicon.svg'));
|
|
});
|
|
|
|
// Página de clientes conectados
|
|
app.get('/clients', (req, res) => {
|
|
res.sendFile(path.join(__dirname, 'public', 'clients.html'));
|
|
});
|
|
|
|
// Página de reset de fábrica
|
|
app.get('/reset', (req, res) => {
|
|
res.sendFile(path.join(__dirname, 'public', 'reset.html'));
|
|
});
|
|
|
|
// Rotas de autenticação (públicas)
|
|
app.use('/api/auth', authRoutes);
|
|
|
|
// Página de login
|
|
app.get('/login', (req, res) => {
|
|
res.sendFile(path.join(__dirname, 'auth', 'login.html'));
|
|
});
|
|
app.use('/login', express.static('auth'));
|
|
|
|
// Serve static files
|
|
app.use(express.static('public'));
|
|
|
|
app.get('/uploads/:filename', async (req, res) => {
|
|
const filename = req.params.filename;
|
|
|
|
// Enviar ETag e Cache-Control imutável
|
|
res.setHeader('ETag', `"${filename}"`);
|
|
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
|
|
|
|
const clientEtag = req.headers['if-none-match'];
|
|
if (clientEtag === `"${filename}"` || clientEtag === filename || clientEtag === `W/"${filename}"`) {
|
|
return res.status(304).end();
|
|
}
|
|
|
|
// Tentar primeiro obter da pasta de uploads (imagens originais)
|
|
let buffer = await storage.getImageBuffer(filename, false);
|
|
|
|
// Se não encontrar, tentar da pasta de processed (imagens rotacionadas/transformadas)
|
|
if (!buffer) {
|
|
buffer = await storage.getImageBuffer(filename, true);
|
|
}
|
|
|
|
if (!buffer) {
|
|
return res.status(404).send('Imagem não encontrada');
|
|
}
|
|
|
|
const ext = path.extname(filename).toLowerCase();
|
|
let contentType = 'image/png';
|
|
if (ext === '.jpg' || ext === '.jpeg') {
|
|
contentType = 'image/jpeg';
|
|
} else if (ext === '.webp') {
|
|
contentType = 'image/webp';
|
|
} else if (ext === '.svg') {
|
|
contentType = 'image/svg+xml';
|
|
}
|
|
|
|
res.setHeader('Content-Type', contentType);
|
|
res.send(buffer);
|
|
});
|
|
|
|
// ================================================================
|
|
// MIDDLEWARE DE VERIFICAÇÃO DE INSTALAÇÃO (APENAS PARA APIs)
|
|
// ================================================================
|
|
|
|
async function checkInstallationMiddleware(req, res, next) {
|
|
// SEMPRE permitir rotas públicas, static files e Socket.IO
|
|
if (req.path === '/' ||
|
|
req.path.startsWith('/install') ||
|
|
req.path.startsWith('/login') ||
|
|
req.path.startsWith('/status') ||
|
|
req.path.startsWith('/socket.io') ||
|
|
req.path.startsWith('/api/auth') ||
|
|
req.path.startsWith('/api/install')) {
|
|
return next();
|
|
}
|
|
|
|
// Para APIs que não são de instalação ou auth, verificar instalação
|
|
if (req.path.startsWith('/api')) {
|
|
try {
|
|
const installedFile = path.join(__dirname, '.installed');
|
|
const fsSync = require('fs');
|
|
|
|
if (!fsSync.existsSync(installedFile)) {
|
|
return res.status(503).json({
|
|
error: 'Sistema não instalado. Acesse /install para configurar.'
|
|
});
|
|
}
|
|
} catch (error) {
|
|
return res.status(503).json({
|
|
error: 'Erro ao verificar instalação.'
|
|
});
|
|
}
|
|
}
|
|
|
|
next();
|
|
}
|
|
|
|
// Aplicar middleware apenas para verificar APIs
|
|
app.use(checkInstallationMiddleware);
|
|
|
|
// ================================================================
|
|
// ROTAS PROTEGIDAS (REQUEREM AUTENTICAÇÃO)
|
|
// ================================================================
|
|
|
|
// APIs de imagens - requerem autenticação
|
|
app.use('/api/images', authenticateToken);
|
|
app.use('/api/images', imageRoutes);
|
|
|
|
// APIs de GTOs - requerem autenticação
|
|
app.use('/api/gtos', authenticateToken);
|
|
app.use('/api/gtos', gtosRouter);
|
|
|
|
app.use('/api/system', authenticateToken);
|
|
app.use('/api/system', systemRoutes);
|
|
|
|
// Endpoint para solicitar a sincronização aos dispositivos conectados
|
|
app.post('/api/system/trigger-sync', authenticateToken, (req, res) => {
|
|
try {
|
|
const windowsClients = connectedClients.filter(c => c.type === 'windows');
|
|
|
|
// Envia o evento 'sync-request' com timestamp para todos os dispositivos clientes identificados
|
|
io.emit('sync-request', { timestamp: Date.now() });
|
|
console.log(`🔄 [Sync] Sinal de sincronização enviado para ${windowsClients.length} dispositivo(s) Windows.`);
|
|
|
|
res.json({
|
|
success: true,
|
|
devicesTriggered: windowsClients.length,
|
|
message: `Sinal de sincronização enviado com sucesso para ${windowsClients.length} dispositivo(s) Windows.`
|
|
});
|
|
} catch (error) {
|
|
console.error('Erro ao solicitar sincronização:', error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// ================================================================
|
|
// SOCKET.IO - CONEXÕES EM TEMPO REAL
|
|
// ================================================================
|
|
|
|
// Middleware de Autenticação para o Socket.IO
|
|
io.use((socket, next) => {
|
|
const token = socket.handshake.auth?.token || socket.handshake.query?.token;
|
|
|
|
// Obter a API KEY do ambiente ou usar uma padrão (fallback local temporário)
|
|
const serverApiKey = process.env.CLIENT_API_KEY || 'rf-dental-secure-key-2026';
|
|
|
|
if (!token) {
|
|
console.log(`❌ Conexão Socket.IO rejeitada (Sem token) - IP: ${socket.handshake.address}`);
|
|
return next(new Error('Authentication error: Token required'));
|
|
}
|
|
|
|
// 1. Verificar se é a API_KEY do Sensor
|
|
if (token === serverApiKey) {
|
|
socket.clientType = 'sensor';
|
|
return next(); // Token válido, prosseguir
|
|
}
|
|
|
|
// 2. Se não for a API_KEY, tentar validar como JWT (Painel Web)
|
|
try {
|
|
const user = jwt.verify(token, JWT_SECRET);
|
|
socket.clientType = 'web';
|
|
socket.user = user;
|
|
return next(); // JWT válido, prosseguir
|
|
} catch (error) {
|
|
console.log(`❌ Conexão Socket.IO rejeitada (Token Inválido) - IP: ${socket.handshake.address}`);
|
|
return next(new Error('Authentication error: Invalid token'));
|
|
}
|
|
});
|
|
|
|
const connectedClients = [];
|
|
|
|
io.on('connection', (socket) => {
|
|
console.log('📱 Cliente conectado:', socket.id);
|
|
console.log('📱 Socket transport:', socket.conn.transport.name);
|
|
console.log('📱 Socket handshake:', JSON.stringify(socket.handshake.query));
|
|
console.log('📱 Remote address:', socket.handshake.address);
|
|
|
|
// Marcar cliente como conectado (mesmo sem identificação)
|
|
socket.isIdentified = false;
|
|
|
|
// Enviar confirmação imediata de conexão
|
|
socket.emit('connection-ack', {
|
|
message: 'Conexão estabelecida com sucesso',
|
|
socketId: socket.id,
|
|
serverTime: new Date().toISOString()
|
|
});
|
|
console.log('✅ Confirmação de conexão enviada para:', socket.id);
|
|
|
|
// Tentar identificar automaticamente após 1 segundo
|
|
setTimeout(() => {
|
|
if (!socket.isIdentified && socket.connected) {
|
|
console.log('⚠️ Cliente ainda não identificado após 1s. Socket ainda conectado:', socket.id);
|
|
// Enviar solicitação de identificação
|
|
socket.emit('please-identify', {
|
|
message: 'Por favor, identifique-se enviando o evento client-identify'
|
|
});
|
|
}
|
|
}, 1000);
|
|
|
|
// Identificação de clientes
|
|
socket.on('client-identify', (data) => {
|
|
console.log('👤 [client-identify] Evento recebido de:', socket.id);
|
|
console.log('👤 Cliente identificado:', data?.type || 'SEM TYPE', data?.name || 'SEM NAME');
|
|
console.log('👤 Dados completos:', JSON.stringify(data, null, 2));
|
|
|
|
if (!data || !data.type) {
|
|
console.error('❌ [client-identify] Dados inválidos:', data);
|
|
socket.emit('error', { message: 'Dados de identificação inválidos' });
|
|
return;
|
|
}
|
|
|
|
const clientInfo = {
|
|
socketId: socket.id,
|
|
type: data.type || 'unknown',
|
|
name: data.name || 'Unknown',
|
|
version: data.version || '1.0.0',
|
|
system: data.system || 'unknown',
|
|
connectedAt: new Date().toISOString()
|
|
};
|
|
|
|
// Remover cliente anterior se existir
|
|
const existingIndex = connectedClients.findIndex(c => c.socketId === socket.id);
|
|
if (existingIndex > -1) {
|
|
connectedClients.splice(existingIndex, 1);
|
|
}
|
|
|
|
connectedClients.push(clientInfo);
|
|
socket.isIdentified = true;
|
|
console.log('✅ Cliente adicionado à lista. Total conectados:', connectedClients.length);
|
|
|
|
// Confirmar para o cliente
|
|
socket.emit('server-identified', {
|
|
success: true,
|
|
message: 'Identificação confirmada',
|
|
clientInfo: clientInfo
|
|
});
|
|
console.log('✅ Confirmação enviada para cliente:', socket.id);
|
|
|
|
// Atualizar lista para todos
|
|
io.emit('clients-list-updated', connectedClients);
|
|
});
|
|
|
|
// Receber imagem do cliente (aceitar mesmo sem identificação prévia)
|
|
socket.on('send-image', async (data, callback) => {
|
|
try {
|
|
console.log('📤 [send-image] Evento recebido de:', socket.id);
|
|
console.log('📤 Cliente identificado?', socket.isIdentified ? 'SIM' : 'NÃO (mas aceitando)');
|
|
console.log('📤 Metadata:', data?.metadata?.fileName || 'SEM METADATA');
|
|
console.log('📤 Patient Data:', data?.patientData?.name || 'SEM PATIENT DATA');
|
|
console.log('📤 Image Base64 length:', data?.imageBase64?.length || 'SEM IMAGEM');
|
|
|
|
// Validar dados mínimos
|
|
if (!data || !data.imageBase64) {
|
|
throw new Error('Dados de imagem incompletos. imageBase64 é obrigatório.');
|
|
}
|
|
|
|
if (!data.metadata || !data.metadata.fileName) {
|
|
throw new Error('Metadata incompleta. fileName é obrigatório.');
|
|
}
|
|
|
|
// Verificar se a imagem já foi inserida no banco
|
|
const existing = await db.get(
|
|
"SELECT id, filename FROM images WHERE original_filename = ? AND enabled = 1 LIMIT 1",
|
|
[data.metadata.fileName]
|
|
);
|
|
if (existing) {
|
|
console.log(`⚠️ Imagem ${data.metadata.fileName} já existe no servidor (ID: ${existing.id}). Ignorando upload.`);
|
|
const ackData = {
|
|
success: true,
|
|
imageId: existing.id,
|
|
message: 'Imagem já existe no servidor',
|
|
filename: existing.filename,
|
|
originalFilename: data.metadata.fileName,
|
|
savedAt: new Date().toISOString()
|
|
};
|
|
socket.emit('image-received', ackData);
|
|
if (typeof callback === 'function') {
|
|
callback(ackData);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Decodificar imagem base64
|
|
const imageBuffer = Buffer.from(data.imageBase64, 'base64');
|
|
|
|
// Validar imagem
|
|
await imageProcessor.validateImage(imageBuffer);
|
|
|
|
// Detectar formato da imagem e gerar extensão adequada
|
|
const imgMetadata = await sharp(imageBuffer).metadata();
|
|
const format = imgMetadata.format || 'png';
|
|
const ext = format === 'jpeg' ? 'jpg' : format;
|
|
|
|
// Salvar imagem original
|
|
const timestamp = Date.now();
|
|
const filename = `${timestamp}_${data.metadata.guid}.${ext}`;
|
|
const filePath = path.join(UPLOAD_DIR, filename);
|
|
|
|
// Buscar nome do cliente se identificado
|
|
const clientInfo = connectedClients.find(c => c.socketId === socket.id);
|
|
const clientName = clientInfo ? clientInfo.name : 'Cliente não identificado';
|
|
const patientName = data.patientData.name || 'Paciente não identificado';
|
|
|
|
await storage.saveImage(filename, imageBuffer, clientName, patientName, false);
|
|
|
|
// Gerar e salvar thumbnail WebP
|
|
let thumbFilename = null;
|
|
try {
|
|
const thumbBuffer = await sharp(imageBuffer)
|
|
.resize({ width: 400, height: 400, fit: 'inside', withoutEnlargement: true })
|
|
.webp({ quality: 80, effort: 4 })
|
|
.toBuffer();
|
|
|
|
thumbFilename = `thumb_${timestamp}_${data.metadata.guid}.webp`;
|
|
await storage.saveImage(thumbFilename, thumbBuffer, clientName, patientName, false);
|
|
console.log(`✅ Thumbnail gerado e salvo no Wasabi: ${thumbFilename}`);
|
|
} catch (thumbErr) {
|
|
console.error('⚠️ Falha ao gerar thumbnail, prosseguindo sem ele:', thumbErr.message);
|
|
}
|
|
|
|
// Determinar data de criação
|
|
const formatLocalDateTime = (date) => {
|
|
const pad = (num) => String(num).padStart(2, '0');
|
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
};
|
|
|
|
const createdAt = (data.patientData && data.patientData.photographTime) || formatLocalDateTime(new Date());
|
|
|
|
// Salvar no banco de dados
|
|
const result = await db.run(
|
|
`INSERT INTO images (
|
|
image_guid,
|
|
patient_name,
|
|
client_name,
|
|
original_filename,
|
|
filename,
|
|
enabled,
|
|
doctor,
|
|
remark,
|
|
thumb_filename,
|
|
created_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
[
|
|
data.metadata.guid,
|
|
data.patientData.name || 'Paciente não identificado',
|
|
clientName,
|
|
data.metadata.fileName,
|
|
filename,
|
|
1,
|
|
data.patientData.doctor || null,
|
|
data.patientData.remark || null,
|
|
thumbFilename,
|
|
createdAt
|
|
]
|
|
);
|
|
|
|
console.log('✅ Imagem salva no banco. ID:', result.lastID);
|
|
console.log('✅ Arquivo salvo em:', filePath);
|
|
|
|
// Confirmar para o cliente
|
|
const ackData = {
|
|
success: true,
|
|
imageId: result.lastID,
|
|
message: 'Imagem recebida com sucesso',
|
|
filename: filename,
|
|
originalFilename: data.metadata.fileName,
|
|
savedAt: new Date().toISOString()
|
|
};
|
|
socket.emit('image-received', ackData);
|
|
if (typeof callback === 'function') {
|
|
callback(ackData);
|
|
}
|
|
console.log('✅ Confirmação enviada para cliente:', socket.id);
|
|
|
|
// Notificar web clients (apenas se não for backlog de sincronização inicial)
|
|
if (!data.isBacklog) {
|
|
io.emit('new-image', {
|
|
id: result.lastID,
|
|
image_guid: data.metadata.guid,
|
|
patient_name: data.patientData.name,
|
|
doctor: data.patientData.doctor || null,
|
|
remark: data.patientData.remark || null,
|
|
filename: filename,
|
|
thumb_filename: thumbFilename,
|
|
created_at: createdAt
|
|
});
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('❌ Erro ao processar imagem:', error);
|
|
const errorData = {
|
|
success: false,
|
|
message: 'Erro ao processar imagem',
|
|
error: error.message
|
|
};
|
|
socket.emit('error', errorData);
|
|
if (typeof callback === 'function') {
|
|
callback(errorData);
|
|
}
|
|
}
|
|
});
|
|
|
|
socket.on('check-image-exists', async (data, callback) => {
|
|
try {
|
|
if (!data || !data.fileName) {
|
|
if (typeof callback === 'function') callback({ success: false, error: 'fileName é obrigatório' });
|
|
return;
|
|
}
|
|
|
|
const existing = await db.get(
|
|
"SELECT id, filename FROM images WHERE original_filename = ? AND enabled = 1 LIMIT 1",
|
|
[data.fileName]
|
|
);
|
|
|
|
if (typeof callback === 'function') {
|
|
callback({
|
|
success: true,
|
|
exists: !!existing,
|
|
imageId: existing ? existing.id : null,
|
|
filename: existing ? existing.filename : null
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('❌ Erro ao verificar existência de imagem:', error);
|
|
if (typeof callback === 'function') callback({ success: false, error: error.message });
|
|
}
|
|
});
|
|
|
|
// Debug: Logar todos os eventos recebidos
|
|
socket.onAny((event, ...args) => {
|
|
if (event !== 'ping' && event !== 'pong') {
|
|
console.log(`🔔 [${socket.id}] Evento recebido: ${event}`, args.length > 0 ? '(com dados)' : '(sem dados)');
|
|
if (args.length > 0 && event !== 'ping') {
|
|
console.log(` Dados:`, JSON.stringify(args[0], null, 2).substring(0, 200));
|
|
}
|
|
}
|
|
});
|
|
|
|
// Monitorar upgrades de transport
|
|
socket.conn.on('upgrade', () => {
|
|
console.log(`🔄 [${socket.id}] Transport upgraded para: ${socket.conn.transport.name}`);
|
|
});
|
|
|
|
// Monitorar erros de conexão
|
|
socket.conn.on('error', (error) => {
|
|
console.error(`❌ [${socket.id}] Erro de conexão:`, error.message);
|
|
});
|
|
|
|
// Heartbeat - manter conexão viva
|
|
socket.on('ping', () => {
|
|
socket.emit('pong', { timestamp: new Date().toISOString() });
|
|
});
|
|
|
|
// Handler para teste de conexão
|
|
socket.on('test-connection', (data) => {
|
|
console.log('🧪 [test-connection] Evento recebido de:', socket.id);
|
|
console.log('🧪 Test ID:', data?.testId);
|
|
|
|
const startTime = data.timestamp || Date.now();
|
|
const latency = Date.now() - startTime;
|
|
|
|
socket.emit('test-connection-ack', {
|
|
testId: data.testId,
|
|
socketId: socket.id,
|
|
latency: latency,
|
|
timestamp: new Date().toISOString(),
|
|
message: 'Teste de conexão bem-sucedido'
|
|
});
|
|
|
|
console.log(`✅ [test-connection] Resposta enviada para ${socket.id}. Latência: ${latency}ms`);
|
|
});
|
|
|
|
// Desconexão
|
|
socket.on('disconnect', (reason) => {
|
|
console.log('📴 Cliente desconectado:', socket.id, '- Motivo:', reason);
|
|
console.log('📴 Estava identificado?', socket.isIdentified ? 'SIM' : 'NÃO');
|
|
|
|
// Remover da lista
|
|
const index = connectedClients.findIndex(c => c.socketId === socket.id);
|
|
if (index > -1) {
|
|
const removed = connectedClients.splice(index, 1);
|
|
console.log('🗑️ Cliente removido da lista:', removed[0].name || socket.id);
|
|
}
|
|
|
|
// Atualizar lista apenas se havia clientes identificados
|
|
if (connectedClients.length > 0) {
|
|
io.emit('clients-list-updated', connectedClients);
|
|
}
|
|
});
|
|
|
|
// Tratamento de erros do socket
|
|
socket.on('error', (error) => {
|
|
console.error(`❌ [${socket.id}] Erro no socket:`, error.message);
|
|
});
|
|
});
|
|
|
|
// ================================================================
|
|
// INICIALIZAÇÃO
|
|
// ================================================================
|
|
|
|
async function initDirectories() {
|
|
try {
|
|
await fs.mkdir(UPLOAD_DIR, { recursive: true });
|
|
await fs.mkdir(PROCESSED_DIR, { recursive: true });
|
|
console.log('✅ Pastas criadas:', UPLOAD_DIR, PROCESSED_DIR);
|
|
} catch (error) {
|
|
console.error('Erro ao criar pastas:', error);
|
|
}
|
|
}
|
|
|
|
async function start() {
|
|
const fsSync = require('fs');
|
|
try {
|
|
// Inicializar diretórios
|
|
await initDirectories();
|
|
|
|
// Inicializar banco se as variáveis já existirem no ambiente
|
|
if (process.env.DB_TYPE === 'sqlite' || (process.env.DB_HOST && process.env.DB_NAME)) {
|
|
try {
|
|
await db.initDatabase();
|
|
console.log('✅ Banco de dados inicializado a partir do ambiente');
|
|
await storage.loadConfigFromDb();
|
|
} catch (error) {
|
|
console.log('❌ Falha ao inicializar o banco de dados:', error.message);
|
|
}
|
|
} else {
|
|
// Fallback para arquivo .env físico
|
|
const envPath = path.join(__dirname, '.env');
|
|
const fsSync = require('fs');
|
|
if (fsSync.existsSync(envPath)) {
|
|
try {
|
|
require('dotenv').config();
|
|
if (process.env.DB_TYPE === 'sqlite' || (process.env.DB_HOST && process.env.DB_NAME)) {
|
|
await db.initDatabase();
|
|
console.log('✅ Banco de dados inicializado a partir do .env');
|
|
await storage.loadConfigFromDb();
|
|
}
|
|
} catch (error) {
|
|
console.log('⚠️ Banco de dados não configurado. Execute a instalação em /install');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Iniciar servidor
|
|
server.listen(PORT, () => {
|
|
console.log('═══════════════════════════════════════════════════════');
|
|
console.log('🚀 DENTAL IMAGE SERVER STARTED');
|
|
console.log('═══════════════════════════════════════════════════════');
|
|
console.log(`📡 Server running on port ${PORT}`);
|
|
console.log(`🌐 Web interface: http://localhost:${PORT}`);
|
|
console.log(`🔧 Installation: http://localhost:${PORT}/install`);
|
|
console.log(`🔐 Login: http://localhost:${PORT}/login`);
|
|
console.log(`📁 Uploads at: ${UPLOAD_DIR}`);
|
|
console.log(`🖼️ Processed at: ${PROCESSED_DIR}`);
|
|
console.log('═══════════════════════════════════════════════════════');
|
|
|
|
const installedFile = path.join(__dirname, '.installed');
|
|
if (!fsSync.existsSync(installedFile)) {
|
|
console.log('⚠️ Sistema não instalado. Acesse /install para configurar.');
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('❌ Erro ao iniciar servidor:', error);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
// ================================================================
|
|
// TRATAMENTO DE ERROS
|
|
// ================================================================
|
|
|
|
process.on('uncaughtException', (error) => {
|
|
console.error('❌ Erro não capturado:', error);
|
|
});
|
|
|
|
process.on('unhandledRejection', (reason, promise) => {
|
|
console.error('❌ Promise rejeitada:', reason);
|
|
});
|
|
|
|
// ================================================================
|
|
// INICIAR
|
|
// ================================================================
|
|
|
|
start();
|