feat: consolidate backend improvements, frontend UX fixes and Wasabi storage support
This commit is contained in:
@@ -72,13 +72,28 @@ router.post('/verify-client-credentials', async (req, res) => {
|
||||
// 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';
|
||||
|
||||
// Se o usuário mandou apenas o Token/API Key (fluxo do Client Windows)
|
||||
if (token) {
|
||||
if (token === serverApiKey) {
|
||||
return res.json({ success: true, message: 'API Key válida' });
|
||||
}
|
||||
|
||||
// Se não for a API Key padrão, tentamos verificar se é um JWT
|
||||
// Verificar se é um machine_token (UUID tem hífen)
|
||||
if (email && password && token.includes('-')) {
|
||||
console.log(`[AUTH] Tentando validar machine_token: ${token} para email: ${email}`);
|
||||
const device = await db.get(`SELECT * FROM clinics_devices WHERE machine_token = $1`, [token]);
|
||||
if (device && device.email === email && device.is_active) {
|
||||
const validPassword = await verifyPassword(password, device.password_hash);
|
||||
console.log(`[AUTH] validPassword=${validPassword}`);
|
||||
if (validPassword) {
|
||||
return res.json({ success: true, message: 'Machine Token e Credenciais válidas', clinic: { name: device.clinic_name, pc: device.pc_name } });
|
||||
}
|
||||
} else {
|
||||
console.log(`[AUTH] Falha: device=${!!device}, is_active=${device?.is_active}`);
|
||||
}
|
||||
return res.status(401).json({ error: 'Credenciais da clínica ou Token inválidos' });
|
||||
}
|
||||
|
||||
// Se não for a API Key padrão nem machine_token, tentamos verificar se é um JWT
|
||||
try {
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { JWT_SECRET } = require('../auth');
|
||||
|
||||
+110
-33
@@ -4,6 +4,75 @@ const db = require('../database');
|
||||
const imageProcessor = require('../image-processor');
|
||||
const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
const storage = require('../storage');
|
||||
|
||||
// ================================================================
|
||||
// GET /api/images/unique-clients - Buscar lista leve de clínicas
|
||||
// ================================================================
|
||||
router.get('/unique-clients', async (req, res) => {
|
||||
try {
|
||||
const clients = await db.all('SELECT DISTINCT client_name FROM images WHERE client_name IS NOT NULL');
|
||||
res.json(clients.map(c => c.client_name));
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ================================================================
|
||||
// POST /api/images/upload - Envio Manual via Web
|
||||
// ================================================================
|
||||
router.post('/upload', async (req, res) => {
|
||||
try {
|
||||
const { imageBase64, patientData } = req.body;
|
||||
|
||||
if (!imageBase64 || !patientData || !patientData.name) {
|
||||
return res.status(400).json({ error: 'Dados incompletos: imageBase64 e patientData.name são obrigatórios.' });
|
||||
}
|
||||
|
||||
const base64Data = imageBase64.replace(/^data:image\/\w+;base64,/, "");
|
||||
const imageBuffer = Buffer.from(base64Data, 'base64');
|
||||
|
||||
await imageProcessor.validateImage(imageBuffer);
|
||||
|
||||
const clientName = 'Upload Manual';
|
||||
const patientName = patientData.name;
|
||||
const timestamp = Date.now();
|
||||
const guid = Math.random().toString(36).substring(2, 15);
|
||||
const filename = `${timestamp}_${guid}.png`;
|
||||
|
||||
await storage.saveImage(filename, imageBuffer, clientName, patientName, false);
|
||||
|
||||
let thumbFilename = null;
|
||||
try {
|
||||
const thumbBuffer = await imageProcessor.getThumbnail(imageBuffer, 400, 400);
|
||||
thumbFilename = `thumb_${filename}`;
|
||||
await storage.saveImage(thumbFilename, thumbBuffer, clientName, patientName, false);
|
||||
} catch (e) {
|
||||
console.error('Erro ao gerar thumbnail manual:', e);
|
||||
}
|
||||
|
||||
const doctor = patientData.doctor || '';
|
||||
const remark = patientData.remark || '';
|
||||
|
||||
const result = await db.run(
|
||||
`INSERT INTO images (
|
||||
image_guid, patient_name, client_name, original_filename, filename, thumb_filename, enabled, doctor, remark
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, 1, $7, $8)`,
|
||||
[guid, patientName, clientName, 'manual_upload.png', filename, thumbFilename, doctor, remark]
|
||||
);
|
||||
|
||||
// Notificar clientes socket para atualizar galeria automaticamente
|
||||
if (req.app.get('io') || global.io) {
|
||||
const io = req.app.get('io') || global.io;
|
||||
if (io) io.emit('new-image', { filename: filename, thumb: thumbFilename, patientName: patientName });
|
||||
}
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Erro no upload manual:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ================================================================
|
||||
// GET /api/images - Listar imagens
|
||||
@@ -54,9 +123,15 @@ router.get('/patients', async (req, res) => {
|
||||
let params = [enabledVal];
|
||||
|
||||
if (clientName) {
|
||||
where += ` AND client_name = $${paramIndex++}`;
|
||||
where += ` AND client_name = $${paramIndex}`;
|
||||
params.push(clientName);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
// Pagination
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const limit = parseInt(req.query.limit) || 50;
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
if (search) {
|
||||
where += ` AND (patient_name LIKE $${paramIndex} OR image_guid LIKE $${paramIndex + 1})`;
|
||||
@@ -73,14 +148,14 @@ router.get('/patients', async (req, res) => {
|
||||
MAX(client_name) AS client_name,
|
||||
COUNT(*) AS image_count,
|
||||
MAX(created_at) AS last_date,
|
||||
(SELECT filename FROM images i2
|
||||
(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`,
|
||||
LIMIT ${limit} OFFSET ${offset}`,
|
||||
params
|
||||
);
|
||||
|
||||
@@ -118,6 +193,28 @@ router.get('/by-patient', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ================================================================
|
||||
// DELETE /api/images/by-patient - Excluir todas 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: 'Parâmetro name obrigatório' });
|
||||
|
||||
const images = await db.all('SELECT * FROM images WHERE patient_name = $1', [patientName]);
|
||||
|
||||
for (const image of images) {
|
||||
await storage.deleteImage(image.filename, image.client_name, image.patient_name);
|
||||
}
|
||||
|
||||
await db.run('DELETE FROM images WHERE patient_name = $1', [patientName]);
|
||||
res.json({ success: true, count: images.length });
|
||||
} catch (error) {
|
||||
console.error('Erro ao excluir imagens do paciente:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ================================================================
|
||||
// GET /api/images/:id - Buscar imagem por ID
|
||||
// ================================================================
|
||||
@@ -139,7 +236,7 @@ router.get('/:id', async (req, res) => {
|
||||
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 = true');
|
||||
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({
|
||||
@@ -168,19 +265,11 @@ router.get('/:id/transformations', async (req, res) => {
|
||||
const originalId = image.original_image_id || image.id;
|
||||
const originalImage = await db.get('SELECT * FROM images WHERE id = $1', [originalId]);
|
||||
|
||||
const originalPath = path.join(__dirname, '../uploads', originalImage.filename);
|
||||
let originalBuffer;
|
||||
|
||||
try {
|
||||
originalBuffer = await fs.readFile(originalPath);
|
||||
originalBuffer = await storage.getImageBuffer(originalImage.filename, originalImage.client_name, originalImage.patient_name, false);
|
||||
} catch (e) {
|
||||
if (e.code === 'ENOENT') {
|
||||
// Fallback for images corrupted by previous bug
|
||||
const fallbackPath = path.join(__dirname, '../processed', originalImage.filename);
|
||||
originalBuffer = await fs.readFile(fallbackPath);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
originalBuffer = await storage.getImageBuffer(originalImage.filename, originalImage.client_name, originalImage.patient_name, true);
|
||||
}
|
||||
|
||||
const transformations = [
|
||||
@@ -232,27 +321,20 @@ router.post('/:id/transform', async (req, res) => {
|
||||
const realOriginal = await db.get('SELECT * FROM images WHERE id = $1', [realOriginalId]);
|
||||
|
||||
// Processar a imagem
|
||||
const originalPath = path.join(__dirname, '../uploads', realOriginal.filename);
|
||||
let originalBuffer;
|
||||
try {
|
||||
originalBuffer = await fs.readFile(originalPath);
|
||||
originalBuffer = await storage.getImageBuffer(realOriginal.filename, realOriginal.client_name, realOriginal.patient_name, false);
|
||||
} catch (e) {
|
||||
if (e.code === 'ENOENT') {
|
||||
const fallbackPath = path.join(__dirname, '../processed', realOriginal.filename);
|
||||
originalBuffer = await fs.readFile(fallbackPath);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
originalBuffer = await storage.getImageBuffer(realOriginal.filename, realOriginal.client_name, realOriginal.patient_name, true);
|
||||
}
|
||||
|
||||
let processed = await imageProcessor.rotate(originalBuffer, rotation);
|
||||
processed = await imageProcessor.flip(processed, flipH, flipV);
|
||||
|
||||
// Salvar nova versão em processed
|
||||
// Salvar nova versão em processed (local e nuvem)
|
||||
const timestamp = Date.now();
|
||||
const processedFilename = `${realOriginal.image_guid}_${timestamp}.png`;
|
||||
const processedPath = path.join(__dirname, '../processed', processedFilename);
|
||||
await fs.writeFile(processedPath, processed);
|
||||
await storage.saveImage(processedFilename, processed, realOriginal.client_name, realOriginal.patient_name, true);
|
||||
|
||||
// Desabilitar a imagem active atual para que não fique duplicada na interface
|
||||
await db.run('UPDATE images SET enabled = 0 WHERE id = $1', [req.params.id]);
|
||||
@@ -305,7 +387,7 @@ router.put('/:id/patient-info', async (req, res) => {
|
||||
patient_name_resumed = $1,
|
||||
tooth_number = $2,
|
||||
tooth_side = $3,
|
||||
enabled = true
|
||||
enabled = 1
|
||||
WHERE id = $4`,
|
||||
[patientNameResumed, toothNumber, toothSide, req.params.id]
|
||||
);
|
||||
@@ -361,13 +443,8 @@ router.delete('/:id', async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
// Apagar arquivo
|
||||
const filePath = path.join(__dirname, '../processed', image.filename);
|
||||
try {
|
||||
await fs.unlink(filePath);
|
||||
} catch (e) {
|
||||
console.warn('Arquivo não encontrado para deletar:', filePath);
|
||||
}
|
||||
// Apagar arquivo local e do S3
|
||||
await storage.deleteImage(image.filename, image.client_name, image.patient_name);
|
||||
|
||||
// Apagar do banco
|
||||
await db.run('DELETE FROM images WHERE id = $1', [req.params.id]);
|
||||
|
||||
@@ -4,6 +4,7 @@ const db = require('../database');
|
||||
const { verifyPassword } = require('../auth');
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const storage = require('../storage');
|
||||
|
||||
// ================================================================
|
||||
// DELETE /api/system/factory-reset - Limpar sistema inteiro
|
||||
@@ -110,6 +111,18 @@ router.get('/storage-config', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ================================================================
|
||||
// GET /api/system/wasabi-status - Retorna status em tempo real
|
||||
// ================================================================
|
||||
router.get('/wasabi-status', (req, res) => {
|
||||
try {
|
||||
const status = storage.getWasabiStatus();
|
||||
res.json(status);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Erro ao verificar status do Wasabi.' });
|
||||
}
|
||||
});
|
||||
|
||||
// ================================================================
|
||||
// POST /api/system/storage-config - Salvar configurações de armazenamento
|
||||
// ================================================================
|
||||
@@ -139,12 +152,31 @@ router.post('/storage-config', async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
const finalAccessKey = wasabiAccessKey.trim();
|
||||
const finalBucket = wasabiBucket.trim();
|
||||
const finalRegion = (wasabiRegion || 'us-east-1').trim();
|
||||
const finalEndpoint = (wasabiEndpoint || 's3.wasabisys.com').trim();
|
||||
|
||||
// 1. Validar as credenciais contra os servidores do Wasabi ANTES de salvar
|
||||
const validation = await storage.validateCredentials(
|
||||
finalAccessKey,
|
||||
finalSecret,
|
||||
finalBucket,
|
||||
finalRegion,
|
||||
finalEndpoint
|
||||
);
|
||||
|
||||
if (!validation.valid) {
|
||||
return res.status(400).json({ error: validation.message });
|
||||
}
|
||||
|
||||
// 2. Se for válido, salvar no banco
|
||||
const configJson = JSON.stringify({
|
||||
wasabiAccessKey: wasabiAccessKey.trim(),
|
||||
wasabiAccessKey: finalAccessKey,
|
||||
wasabiSecretKey: finalSecret,
|
||||
wasabiBucket: wasabiBucket.trim(),
|
||||
wasabiRegion: (wasabiRegion || 'us-east-1').trim(),
|
||||
wasabiEndpoint: (wasabiEndpoint || 's3.wasabisys.com').trim()
|
||||
wasabiBucket: finalBucket,
|
||||
wasabiRegion: finalRegion,
|
||||
wasabiEndpoint: finalEndpoint
|
||||
});
|
||||
|
||||
await db.run(`
|
||||
|
||||
Reference in New Issue
Block a user