feat: adicionar gerenciamento de clínicas e tokens
This commit is contained in:
@@ -48,7 +48,10 @@ router.post('/login', async (req, res) => {
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email
|
||||
email: user.email,
|
||||
clinic_name: user.clinic_name,
|
||||
pc_name: user.pc_name,
|
||||
is_admin: user.is_admin
|
||||
}
|
||||
});
|
||||
|
||||
@@ -85,7 +88,10 @@ router.get('/verify', async (req, res) => {
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email
|
||||
email: user.email,
|
||||
clinic_name: user.clinic_name,
|
||||
pc_name: user.pc_name,
|
||||
is_admin: user.is_admin
|
||||
}
|
||||
});
|
||||
|
||||
@@ -109,13 +115,17 @@ router.post('/logout', (req, res) => {
|
||||
// POST /api/auth/create-user - Criar usuário (admin only)
|
||||
// ================================================================
|
||||
|
||||
router.post('/create-user', async (req, res) => {
|
||||
router.post('/create-user', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { username, email, password } = req.body;
|
||||
if (!req.user.is_admin) {
|
||||
return res.status(403).json({ error: 'Apenas administradores podem criar usuários' });
|
||||
}
|
||||
|
||||
const { username, email, password, clinic_name, pc_name } = req.body;
|
||||
|
||||
if (!username || !email || !password) {
|
||||
return res.status(400).json({
|
||||
error: 'Todos os campos são obrigatórios'
|
||||
error: 'Username, email e senha são obrigatórios'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -136,9 +146,9 @@ router.post('/create-user', async (req, res) => {
|
||||
|
||||
// Criar usuário
|
||||
const result = await db.run(
|
||||
`INSERT INTO users (username, email, password_hash, created_at)
|
||||
VALUES (?, ?, ?, NOW())`,
|
||||
[username, email, passwordHash]
|
||||
`INSERT INTO users (username, email, password_hash, clinic_name, pc_name, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, NOW())`,
|
||||
[username, email, passwordHash, clinic_name || null, pc_name || null]
|
||||
);
|
||||
|
||||
res.json({
|
||||
@@ -153,6 +163,54 @@ router.post('/create-user', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ================================================================
|
||||
// GERENCIAMENTO DE USUÁRIOS/CLÍNICAS (ADMIN)
|
||||
// ================================================================
|
||||
|
||||
router.get('/users', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
if (!req.user.is_admin) {
|
||||
return res.status(403).json({ error: 'Acesso negado' });
|
||||
}
|
||||
const users = await db.all('SELECT id, username, email, clinic_name, pc_name, is_admin, created_at FROM users ORDER BY created_at DESC');
|
||||
res.json({ success: true, users });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Erro ao buscar usuários' });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/users/:id', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
if (!req.user.is_admin) {
|
||||
return res.status(403).json({ error: 'Acesso negado' });
|
||||
}
|
||||
const userId = req.params.id;
|
||||
if (parseInt(userId) === req.user.id) {
|
||||
return res.status(400).json({ error: 'Não é possível excluir o próprio usuário admin' });
|
||||
}
|
||||
await db.run('DELETE FROM users WHERE id = ?', [userId]);
|
||||
res.json({ success: true, message: 'Usuário excluído com sucesso' });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Erro ao excluir usuário' });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/users/:id/token', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
if (!req.user.is_admin) {
|
||||
return res.status(403).json({ error: 'Acesso negado' });
|
||||
}
|
||||
const user = await db.get('SELECT * FROM users WHERE id = ?', [req.params.id]);
|
||||
if (!user) return res.status(404).json({ error: 'Usuário não encontrado' });
|
||||
|
||||
// Gera um token com duração longa para uso no desktop (ex: 3650 dias = ~10 anos)
|
||||
const token = generateToken(user, '3650d');
|
||||
res.json({ success: true, token });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Erro ao gerar token' });
|
||||
}
|
||||
});
|
||||
|
||||
// ================================================================
|
||||
// PUT /api/auth/update-credentials - Alterar usuário/senha
|
||||
// ================================================================
|
||||
|
||||
+36
-154
@@ -4,8 +4,6 @@ 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
|
||||
@@ -66,12 +64,12 @@ router.get('/patients', async (req, res) => {
|
||||
const rows = await db.all(
|
||||
`SELECT
|
||||
patient_name,
|
||||
MAX(doctor) AS doctor,
|
||||
MAX(remark) AS remark,
|
||||
MAX(client_name) AS client_name,
|
||||
doctor,
|
||||
remark,
|
||||
client_name,
|
||||
COUNT(*) AS image_count,
|
||||
MAX(created_at) AS last_date,
|
||||
(SELECT COALESCE(thumb_filename, filename) FROM images i2
|
||||
(SELECT 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
|
||||
@@ -116,107 +114,6 @@ router.get('/by-patient', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ================================================================
|
||||
// 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
|
||||
// ================================================================
|
||||
@@ -267,13 +164,19 @@ 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 = ?', [originalId]);
|
||||
|
||||
let originalBuffer = await storage.getImageBuffer(originalImage.filename, false);
|
||||
if (!originalBuffer) {
|
||||
originalBuffer = await storage.getImageBuffer(originalImage.filename, true);
|
||||
}
|
||||
const originalPath = path.join(__dirname, '../uploads', originalImage.filename);
|
||||
let originalBuffer;
|
||||
|
||||
if (!originalBuffer) {
|
||||
return res.status(404).json({ error: 'Arquivo de imagem original não encontrado' });
|
||||
try {
|
||||
originalBuffer = await fs.readFile(originalPath);
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
||||
const transformations = [
|
||||
@@ -325,13 +228,17 @@ router.post('/:id/transform', async (req, res) => {
|
||||
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' });
|
||||
const originalPath = path.join(__dirname, '../uploads', realOriginal.filename);
|
||||
let originalBuffer;
|
||||
try {
|
||||
originalBuffer = await fs.readFile(originalPath);
|
||||
} catch (e) {
|
||||
if (e.code === 'ENOENT') {
|
||||
const fallbackPath = path.join(__dirname, '../processed', realOriginal.filename);
|
||||
originalBuffer = await fs.readFile(fallbackPath);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
let processed = await imageProcessor.rotate(originalBuffer, rotation);
|
||||
@@ -340,7 +247,8 @@ router.post('/:id/transform', async (req, res) => {
|
||||
// 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);
|
||||
const processedPath = path.join(__dirname, '../processed', processedFilename);
|
||||
await fs.writeFile(processedPath, processed);
|
||||
|
||||
// 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]);
|
||||
@@ -431,37 +339,6 @@ router.get('/:id/download', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ================================================================
|
||||
// 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
|
||||
// ================================================================
|
||||
@@ -480,8 +357,13 @@ router.delete('/:id', async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
// Apagar arquivo do storage (Wasabi S3 + Local)
|
||||
await storage.deleteImage(image.filename, image.client_name, image.patient_name);
|
||||
// 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 do banco
|
||||
await db.run('DELETE FROM images WHERE id = ?', [req.params.id]);
|
||||
|
||||
@@ -68,29 +68,4 @@ router.delete('/factory-reset', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ================================================================
|
||||
// GET /api/system/storage-config - Obter configurações de storage
|
||||
// ================================================================
|
||||
router.get('/storage-config', (req, res) => {
|
||||
try {
|
||||
const storage = require('../storage');
|
||||
res.json(storage.getSafeConfig());
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ================================================================
|
||||
// POST /api/system/storage-config - Atualizar configurações de storage
|
||||
// ================================================================
|
||||
router.post('/storage-config', async (req, res) => {
|
||||
try {
|
||||
const storage = require('../storage');
|
||||
const result = await storage.updateConfig(req.body);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
Reference in New Issue
Block a user