feat: adicionar gerenciamento de clínicas e tokens

This commit is contained in:
VPS 4 Builder
2026-05-26 02:59:11 +02:00
parent 5e86717d12
commit 75c4bf3f87
14 changed files with 1241 additions and 2123 deletions
+66 -8
View File
@@ -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
View File
@@ -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]);
-25
View File
@@ -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;