Files
rx.scoreodonto.com/dental-server/routes/images.js
T

516 lines
18 KiB
JavaScript

const express = require('express');
const router = express.Router();
const db = require('../database');
const imageProcessor = require('../image-processor');
const path = require('path');
const fs = require('fs').promises;
const storage = require('../storage');
const sharp = require('sharp');
// ================================================================
// GET /api/images - Listar imagens
// ================================================================
router.get('/', async (req, res) => {
try {
const showDisabled = req.query.disabled === 'true';
const search = req.query.search || '';
const clientName = req.query.client || '';
let query = `SELECT * FROM images WHERE enabled = ?`;
let params = [showDisabled ? 0 : 1];
if (clientName) {
query += ` AND client_name = ?`;
params.push(clientName);
}
if (search) {
query += ` AND (patient_name LIKE ? OR image_guid LIKE ? OR filename LIKE ?)`;
params.push(`%${search}%`, `%${search}%`, `%${search}%`);
}
query += ` ORDER BY created_at DESC LIMIT 100`;
const images = await db.all(query, params);
res.json(images);
} catch (error) {
console.error('Erro ao listar imagens:', error);
res.status(500).json({ error: error.message });
}
});
// ================================================================
// GET /api/images/patients - Listar pacientes agrupados
// ================================================================
router.get('/patients', async (req, res) => {
try {
const showDisabled = req.query.disabled === 'true';
const search = req.query.search || '';
const clientName = req.query.client || '';
const enabledVal = showDisabled ? 0 : 1;
let where = `WHERE enabled = ${enabledVal}`;
let params = [];
if (clientName) {
where += ` AND client_name = ?`;
params.push(clientName);
}
if (search) {
where += ` AND (patient_name LIKE ? OR image_guid LIKE ?)`;
params.push(`%${search}%`, `%${search}%`);
}
// Group by patient_name — pick thumbnail from most recent image
const rows = await db.all(
`SELECT
patient_name,
MAX(doctor) AS doctor,
MAX(remark) AS remark,
MAX(client_name) AS client_name,
COUNT(*) AS image_count,
MAX(created_at) AS last_date,
(SELECT COALESCE(thumb_filename, filename) FROM images i2
WHERE i2.patient_name = images.patient_name AND i2.enabled = ${enabledVal}
ORDER BY i2.created_at DESC LIMIT 1) AS thumb_filename
FROM images
${where}
GROUP BY patient_name
ORDER BY MAX(created_at) DESC
LIMIT 200`,
params
);
res.json(rows);
} catch (error) {
console.error('Erro ao listar pacientes:', error);
res.status(500).json({ error: error.message });
}
});
// ================================================================
// GET /api/images/by-patient - Imagens de um paciente
// ================================================================
router.get('/by-patient', async (req, res) => {
try {
const patientName = req.query.name || '';
const showDisabled = req.query.disabled === 'true';
if (!patientName) {
return res.status(400).json({ error: 'Parâmetro name obrigatório' });
}
const images = await db.all(
`SELECT * FROM images
WHERE patient_name = ? AND enabled = ?
ORDER BY created_at DESC`,
[patientName, showDisabled ? 0 : 1]
);
res.json(images);
} catch (error) {
console.error('Erro ao buscar imagens do paciente:', error);
res.status(500).json({ error: error.message });
}
});
// ================================================================
// POST /api/images/upload - Fazer upload manual de imagem
// ================================================================
router.post('/upload', async (req, res) => {
try {
const { imageBase64, filename, patientName, doctor, remark } = req.body;
if (!imageBase64) {
return res.status(400).json({ error: 'imageBase64 é obrigatório' });
}
// Decodificar imagem base64
let base64Data = imageBase64;
if (base64Data.includes(';base64,')) {
base64Data = base64Data.split(';base64,')[1];
}
const imageBuffer = Buffer.from(base64Data, 'base64');
// TODO(security): Adicionado limite de 10MB para prevenir Buffer Overflow/DoS
if (imageBuffer.length > 10 * 1024 * 1024) {
return res.status(400).json({ error: 'A imagem excede o limite de tamanho permitido de 10MB.' });
}
// Validar imagem (garante integridade do arquivo através do sharp)
await imageProcessor.validateImage(imageBuffer);
// Salvar imagem
const imageGuid = 'web_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
// Detectar formato
const imgMetadata = await sharp(imageBuffer).metadata();
const format = imgMetadata.format || 'png';
const ext = format === 'jpeg' ? 'jpg' : format;
const savedFilename = `${Date.now()}_${imageGuid}.${ext}`;
const clientName = 'Upload Web';
const patientVal = patientName ? patientName.trim() : 'Paciente não identificado';
await storage.saveImage(savedFilename, imageBuffer, clientName, patientVal, false);
// Gerar 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_${Date.now()}_${imageGuid}.webp`;
await storage.saveImage(thumbFilename, thumbBuffer, clientName, patientVal, false);
console.log(`✅ Thumbnail gerado e salvo para upload manual: ${thumbFilename}`);
} catch (thumbErr) {
console.error('⚠️ Falha ao gerar thumbnail para upload manual:', thumbErr.message);
}
// Inserir no banco de dados
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 = formatLocalDateTime(new Date());
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
imageGuid,
patientVal,
clientName,
filename || 'upload_manual.png',
savedFilename,
1,
doctor || null,
remark || null,
thumbFilename,
createdAt
]
);
res.json({
success: true,
imageId: result.lastID,
filename: savedFilename,
message: 'Imagem enviada com sucesso!'
});
} catch (error) {
console.error('Erro no upload de imagem manual:', error);
res.status(500).json({ error: error.message });
}
});
// ================================================================
// GET /api/images/:id - Buscar imagem por ID
// ================================================================
router.get('/:id', async (req, res) => {
try {
const image = await db.get('SELECT * FROM images WHERE id = ?', [req.params.id]);
if (!image) {
return res.status(404).json({ error: 'Imagem não encontrada' });
}
res.json(image);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// ================================================================
// GET /api/images/stats - Estatísticas
// ================================================================
router.get('/stats', async (req, res) => {
try {
const total = await db.get('SELECT COUNT(*) as count FROM images');
const enabled = await db.get('SELECT COUNT(*) as count FROM images WHERE enabled = 1');
const withTooth = await db.get('SELECT COUNT(*) as count FROM images WHERE tooth_number IS NOT NULL');
res.json({
total: total.count,
enabled: enabled.count,
disabled: total.count - enabled.count,
withToothInfo: withTooth.count,
withoutToothInfo: total.count - withTooth.count
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// ================================================================
// GET /api/images/:id/transformations - Gerar transformações
// ================================================================
router.get('/:id/transformations', async (req, res) => {
try {
const image = await db.get('SELECT * FROM images WHERE id = ?', [req.params.id]);
if (!image) {
return res.status(404).json({ error: 'Imagem não encontrada' });
}
// Buscar a imagem original se esta é uma transformação
const originalId = image.original_image_id || image.id;
const originalImage = await db.get('SELECT * FROM images WHERE id = ?', [originalId]);
let originalBuffer = await storage.getImageBuffer(originalImage.filename, false);
if (!originalBuffer) {
originalBuffer = await storage.getImageBuffer(originalImage.filename, true);
}
if (!originalBuffer) {
return res.status(404).json({ error: 'Arquivo de imagem original não encontrado' });
}
const transformations = [
{ name: 'Original', rotation: 0, flipH: false, flipV: false },
{ name: 'Flip Horizontal', rotation: 0, flipH: true, flipV: false },
{ name: 'Flip Vertical', rotation: 0, flipH: false, flipV: true },
{ name: 'Rotação +90°', rotation: 90, flipH: false, flipV: false },
{ name: 'Rotação -90°', rotation: -90, flipH: false, flipV: false }
];
const results = [];
for (const t of transformations) {
try {
let processed = await imageProcessor.rotate(originalBuffer, t.rotation);
processed = await imageProcessor.flip(processed, t.flipH, t.flipV);
const base64 = `data:image/png;base64,${processed.toString('base64')}`;
results.push({
...t,
base64,
size: processed.length
});
} catch (error) {
console.error(`Erro ao processar transformação ${t.name}:`, error);
}
}
res.json(results);
} catch (error) {
console.error('Erro ao gerar transformações:', error);
res.status(500).json({ error: error.message });
}
});
// ================================================================
// POST /api/images/:id/transform - Salvar transformação
// ================================================================
router.post('/:id/transform', async (req, res) => {
try {
const { rotation, flipH, flipV, remark } = req.body;
const originalImage = await db.get('SELECT * FROM images WHERE id = ?', [req.params.id]);
if (!originalImage) {
return res.status(404).json({ error: 'Imagem não encontrada' });
}
// Buscar a imagem original real se esta é uma transformação
const realOriginalId = originalImage.original_image_id || originalImage.id;
const realOriginal = await db.get('SELECT * FROM images WHERE id = ?', [realOriginalId]);
// Processar a imagem
let originalBuffer = await storage.getImageBuffer(realOriginal.filename, false);
if (!originalBuffer) {
originalBuffer = await storage.getImageBuffer(realOriginal.filename, true);
}
if (!originalBuffer) {
return res.status(404).json({ error: 'Arquivo de imagem original não encontrado' });
}
let processed = await imageProcessor.rotate(originalBuffer, rotation);
processed = await imageProcessor.flip(processed, flipH, flipV);
// Salvar nova versão em processed
const timestamp = Date.now();
const processedFilename = `${realOriginal.image_guid}_${timestamp}.png`;
await storage.saveImage(processedFilename, processed, realOriginal.client_name, realOriginal.patient_name, true);
// Desabilitar a imagem atual para que não fique duplicada na interface
await db.run('UPDATE images SET enabled = 0 WHERE id = ?', [req.params.id]);
const newRemark = remark !== undefined ? remark : originalImage.remark;
// Inserir a nova imagem como a versão ativa
const result = await db.run(
`INSERT INTO images (
image_guid, patient_name, client_name, original_filename, filename,
enabled, rotation, flip_horizontal, flip_vertical, original_image_id, created_at, remark, doctor
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
`${realOriginal.image_guid}_${timestamp}`,
realOriginal.patient_name,
realOriginal.client_name,
realOriginal.filename,
processedFilename,
1, // Nova imagem fica ativa
rotation,
flipH ? 1 : 0,
flipV ? 1 : 0,
realOriginalId,
originalImage.created_at, // Manter a data de criação original
newRemark,
realOriginal.doctor
]
);
res.json({
success: true,
imageId: result.lastID,
message: 'Orientação atualizada com sucesso!'
});
} catch (error) {
console.error('Erro ao salvar transformação:', error);
res.status(500).json({ error: error.message });
}
});
// ================================================================
// PUT /api/images/:id/patient-info - Atualizar dados do paciente
// ================================================================
router.put('/:id/patient-info', async (req, res) => {
try {
const { patientNameResumed, toothNumber, toothSide } = req.body;
await db.run(
`UPDATE images SET
patient_name_resumed = ?,
tooth_number = ?,
tooth_side = ?,
enabled = 1
WHERE id = ?`,
[patientNameResumed, toothNumber, toothSide, req.params.id]
);
res.json({ success: true, message: 'Dados do paciente atualizados!' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// ================================================================
// GET /api/images/:id/download - Download da imagem
// ================================================================
router.get('/:id/download', async (req, res) => {
try {
const image = await db.get('SELECT * FROM images WHERE id = ?', [req.params.id]);
if (!image) {
return res.status(404).json({ error: 'Imagem não encontrada' });
}
// Nome: nome-resumido_numero_lado.png
if (image.patient_name_resumed && image.tooth_number && image.tooth_side) {
const filename = `${image.patient_name_resumed}_${image.tooth_number}_${image.tooth_side}.png`;
const filePath = path.join(__dirname, '../processed', image.filename);
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
res.download(filePath, filename);
} else {
res.status(400).json({ error: 'Dados do paciente incompletos' });
}
} catch (error) {
console.error('Erro no download:', error);
res.status(500).json({ error: error.message });
}
});
// ================================================================
// DELETE /api/images/by-patient - Apagar todas as imagens de um paciente
// ================================================================
router.delete('/by-patient', async (req, res) => {
try {
const patientName = req.query.name || '';
if (!patientName) {
return res.status(400).json({ error: 'Nome do paciente é obrigatório' });
}
// Buscar todas as imagens do paciente no banco de dados
const images = await db.all('SELECT filename, client_name, patient_name FROM images WHERE patient_name = ?', [patientName]);
// Apagar cada imagem do storage (Wasabi S3 + Local)
for (const img of images) {
await storage.deleteImage(img.filename, img.client_name, img.patient_name);
}
// Apagar os registros do banco de dados
await db.run('DELETE FROM images WHERE patient_name = ?', [patientName]);
res.json({
success: true,
message: `Todas as imagens do paciente "${patientName}" foram excluídas com sucesso do servidor e do Wasabi.`
});
} catch (error) {
console.error('Erro ao excluir imagens do paciente em lote:', error);
res.status(500).json({ error: error.message });
}
});
// ================================================================
// DELETE /api/images/:id - Apagar versão da imagem
// ================================================================
router.delete('/:id', async (req, res) => {
try {
const image = await db.get('SELECT * FROM images WHERE id = ?', [req.params.id]);
if (!image) {
return res.status(404).json({ error: 'Imagem não encontrada' });
}
// NUNCA apagar a original
if (!image.original_image_id) {
return res.status(400).json({
error: 'Não é possível apagar a imagem original',
code: 'ORIGINAL_NOT_DELETABLE'
});
}
// Apagar arquivo do storage (Wasabi S3 + Local)
await storage.deleteImage(image.filename, image.client_name, image.patient_name);
// Apagar do banco
await db.run('DELETE FROM images WHERE id = ?', [req.params.id]);
res.json({ success: true, message: 'Imagem apagada com sucesso' });
} catch (error) {
console.error('Erro ao apagar imagem:', error);
res.status(500).json({ error: error.message });
}
});
// ================================================================
// PUT /api/images/:id/toggle-enabled - Habilitar/Desabilitar
// ================================================================
router.put('/:id/toggle-enabled', async (req, res) => {
try {
const image = await db.get('SELECT * FROM images WHERE id = ?', [req.params.id]);
if (!image) {
return res.status(404).json({ error: 'Imagem não encontrada' });
}
const newEnabled = image.enabled ? 0 : 1;
await db.run('UPDATE images SET enabled = ? WHERE id = ?', [newEnabled, req.params.id]);
res.json({ success: true, enabled: newEnabled === 1 });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;