feat: reestruturacao de menus na sidebar, niveis de acesso e migracao para postgresql/dragonflydb
continuous-integration/webhook Deploy concluido (rx.scoreodonto.com)

This commit is contained in:
VPS 4 Builder
2026-05-26 21:08:48 +02:00
parent dc1801f4a7
commit 5b0d80bf22
29 changed files with 257 additions and 7187 deletions
+4 -4
View File
@@ -41,7 +41,7 @@ router.post('/', requireAdmin, async (req, res) => {
try {
// Verificar se email já existe
const existing = await db.get(`SELECT id FROM clinics_devices WHERE email = ?`, [email]);
const existing = await db.get(`SELECT id FROM clinics_devices WHERE email = $1`, [email]);
if (existing) {
return res.status(400).json({ success: false, message: 'Email já cadastrado para outra clínica' });
}
@@ -52,7 +52,7 @@ router.post('/', requireAdmin, async (req, res) => {
await db.run(`
INSERT INTO clinics_devices (clinic_name, email, password_hash, pc_name, machine_token)
VALUES (?, ?, ?, ?, ?)
VALUES ($1, $2, $3, $4, $5) RETURNING id
`, [clinic_name, email, password_hash, pc_name, machine_token]);
res.json({
@@ -74,7 +74,7 @@ router.put('/:id/status', requireAdmin, async (req, res) => {
const { is_active } = req.body;
try {
await db.run(`UPDATE clinics_devices SET is_active = ? WHERE id = ?`, [is_active ? 1 : 0, id]);
await db.run(`UPDATE clinics_devices SET is_active = $1 WHERE id = $2`, [is_active ? 1 : 0, id]);
res.json({ success: true, message: 'Status atualizado com sucesso' });
} catch (error) {
res.status(500).json({ success: false, message: 'Erro interno' });
@@ -88,7 +88,7 @@ router.delete('/:id', requireAdmin, async (req, res) => {
const { id } = req.params;
try {
await db.run(`DELETE FROM clinics_devices WHERE id = ?`, [id]);
await db.run(`DELETE FROM clinics_devices WHERE id = $1`, [id]);
res.json({ success: true, message: 'Clínica excluída com sucesso' });
} catch (error) {
res.status(500).json({ success: false, message: 'Erro interno' });
+10 -10
View File
@@ -19,7 +19,7 @@ router.post('/login', async (req, res) => {
// Buscar usuário
const user = await db.get(
'SELECT * FROM users WHERE username = ? OR email = ?',
'SELECT * FROM users WHERE username = $1 OR email = $2',
[username, username]
);
@@ -77,7 +77,7 @@ router.get('/verify', async (req, res) => {
const jwt = require('jsonwebtoken');
const decoded = jwt.verify(token, require('../auth').JWT_SECRET);
const user = await db.get('SELECT * FROM users WHERE id = ?', [decoded.id]);
const user = await db.get('SELECT * FROM users WHERE id = $1', [decoded.id]);
if (!user) {
return res.status(404).json({ error: 'Usuário não encontrado' });
@@ -131,7 +131,7 @@ router.post('/create-user', authenticateToken, async (req, res) => {
// Verificar se usuário já existe
const existing = await db.get(
'SELECT * FROM users WHERE username = ? OR email = ?',
'SELECT * FROM users WHERE username = $1 OR email = $2',
[username, email]
);
@@ -147,7 +147,7 @@ router.post('/create-user', authenticateToken, async (req, res) => {
// Criar usuário
const result = await db.run(
`INSERT INTO users (username, email, password_hash, clinic_name, pc_name, is_admin, created_at)
VALUES (?, ?, ?, ?, ?, ?, NOW())`,
VALUES ($1, $2, $3, $4, $5, $6, CURRENT_TIMESTAMP) RETURNING id`,
[username, email, passwordHash, clinic_name || null, pc_name || null, !!is_admin]
);
@@ -188,7 +188,7 @@ router.delete('/users/:id', authenticateToken, async (req, res) => {
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]);
await db.run('DELETE FROM users WHERE id = $1', [userId]);
res.json({ success: true, message: 'Usuário excluído com sucesso' });
} catch (error) {
res.status(500).json({ error: 'Erro ao excluir usuário' });
@@ -200,7 +200,7 @@ router.post('/users/:id/token', authenticateToken, async (req, res) => {
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]);
const user = await db.get('SELECT * FROM users WHERE id = $1', [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)
@@ -229,7 +229,7 @@ router.put('/update-credentials', authenticateToken, async (req, res) => {
}
// Buscar usuário atual para verificar a senha
const user = await db.get('SELECT * FROM users WHERE id = ?', [userId]);
const user = await db.get('SELECT * FROM users WHERE id = $1', [userId]);
if (!user) {
return res.status(404).json({ error: 'Usuário não encontrado' });
}
@@ -246,7 +246,7 @@ router.put('/update-credentials', authenticateToken, async (req, res) => {
// Verificar e aplicar novo username se fornecido
if (newUsername && newUsername !== user.username) {
const existing = await db.get('SELECT * FROM users WHERE username = ? AND id != ?', [newUsername, userId]);
const existing = await db.get('SELECT * FROM users WHERE username = $1 AND id != $2', [newUsername, userId]);
if (existing) {
return res.status(409).json({ error: 'Este nome de usuário já está em uso' });
}
@@ -255,7 +255,7 @@ router.put('/update-credentials', authenticateToken, async (req, res) => {
// Verificar e aplicar novo e-mail se fornecido
if (newEmail && newEmail !== user.email) {
const existing = await db.get('SELECT * FROM users WHERE email = ? AND id != ?', [newEmail, userId]);
const existing = await db.get('SELECT * FROM users WHERE email = $1 AND id != $2', [newEmail, userId]);
if (existing) {
return res.status(409).json({ error: 'Este e-mail já está em uso' });
}
@@ -272,7 +272,7 @@ router.put('/update-credentials', authenticateToken, async (req, res) => {
// Atualizar no banco de dados
await db.run(
'UPDATE users SET username = ?, email = ?, password_hash = ? WHERE id = ?',
'UPDATE users SET username = $1, email = $2, password_hash = $3 WHERE id = $4',
[finalUsername, finalEmail, finalPasswordHash, userId]
);
+2 -2
View File
@@ -22,7 +22,7 @@ router.post('/login', async (req, res) => {
try {
// 1. Buscar o dispositivo pelo Token
const device = await db.get(`SELECT * FROM clinics_devices WHERE machine_token = ?`, [machine_token]);
const device = await db.get(`SELECT * FROM clinics_devices WHERE machine_token = $1`, [machine_token]);
if (!device) {
return res.status(401).json({ success: false, message: 'Token da máquina inválido ou inexistente' });
@@ -46,7 +46,7 @@ router.post('/login', async (req, res) => {
// Atualizar último IP conectado
const clientIp = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
await db.run(`UPDATE clinics_devices SET last_ip = ? WHERE id = ?`, [clientIp, device.id]);
await db.run(`UPDATE clinics_devices SET last_ip = $1 WHERE id = $2`, [clientIp, device.id]);
// 5. Gerar Token JWT de Sessão
// O Token vale por 2 horas, obrigando o cliente a re-logar ou pedir refresh
+12 -12
View File
@@ -15,7 +15,7 @@ router.get('/', async (req, res) => {
`SELECT g.*, COUNT(gi.id) as image_count
FROM gtos g
LEFT JOIN gto_images gi ON gi.gto_id = g.id
WHERE g.patient_name = ?
WHERE g.patient_name = $1
GROUP BY g.id
ORDER BY g.created_at DESC`,
[patient]
@@ -42,14 +42,14 @@ router.get('/', async (req, res) => {
// ================================================================
router.get('/:id', async (req, res) => {
try {
const gto = await db.get('SELECT * FROM gtos WHERE id = ?', [req.params.id]);
const gto = await db.get('SELECT * FROM gtos WHERE id = $1', [req.params.id]);
if (!gto) return res.status(404).json({ error: 'GTO não encontrada' });
const images = await db.all(
`SELECT i.*, gi.id as link_id
FROM gto_images gi
JOIN images i ON i.id = gi.image_id
WHERE gi.gto_id = ?
WHERE gi.gto_id = $1
ORDER BY gi.created_at DESC`,
[req.params.id]
);
@@ -71,10 +71,10 @@ router.post('/', async (req, res) => {
return res.status(400).json({ error: 'gto_number e patient_name são obrigatórios' });
}
const result = await db.run(
`INSERT INTO gtos (gto_number, patient_name, description) VALUES (?, ?, ?)`,
`INSERT INTO gtos (gto_number, patient_name, description) VALUES ($1, $2, $3) RETURNING id`,
[gto_number.trim(), patient_name.trim(), description || '']
);
const gto = await db.get('SELECT * FROM gtos WHERE id = ?', [result.lastID]);
const gto = await db.get('SELECT * FROM gtos WHERE id = $1', [result.lastID]);
res.status(201).json(gto);
} catch (err) {
console.error('Erro ao criar GTO:', err);
@@ -89,10 +89,10 @@ router.put('/:id', async (req, res) => {
try {
const { gto_number, description } = req.body;
await db.run(
`UPDATE gtos SET gto_number = ?, description = ? WHERE id = ?`,
`UPDATE gtos SET gto_number = $1, description = $2 WHERE id = $3`,
[gto_number, description || '', req.params.id]
);
const gto = await db.get('SELECT * FROM gtos WHERE id = ?', [req.params.id]);
const gto = await db.get('SELECT * FROM gtos WHERE id = $1', [req.params.id]);
res.json(gto);
} catch (err) {
res.status(500).json({ error: err.message });
@@ -104,8 +104,8 @@ router.put('/:id', async (req, res) => {
// ================================================================
router.delete('/:id', async (req, res) => {
try {
await db.run('DELETE FROM gto_images WHERE gto_id = ?', [req.params.id]);
await db.run('DELETE FROM gtos WHERE id = ?', [req.params.id]);
await db.run('DELETE FROM gto_images WHERE gto_id = $1', [req.params.id]);
await db.run('DELETE FROM gtos WHERE id = $1', [req.params.id]);
res.json({ success: true });
} catch (err) {
res.status(500).json({ error: err.message });
@@ -123,13 +123,13 @@ router.post('/:id/images', async (req, res) => {
// Verificar se já está vinculada
const existing = await db.get(
'SELECT id FROM gto_images WHERE gto_id = ? AND image_id = ?',
'SELECT id FROM gto_images WHERE gto_id = $1 AND image_id = $2',
[req.params.id, image_id]
);
if (existing) return res.status(409).json({ error: 'Imagem já vinculada a esta GTO' });
await db.run(
'INSERT INTO gto_images (gto_id, image_id) VALUES (?, ?)',
'INSERT INTO gto_images (gto_id, image_id) VALUES ($1, $2) RETURNING id',
[req.params.id, image_id]
);
res.status(201).json({ success: true, message: 'Imagem vinculada à GTO' });
@@ -145,7 +145,7 @@ router.post('/:id/images', async (req, res) => {
router.delete('/:id/images/:imageId', async (req, res) => {
try {
await db.run(
'DELETE FROM gto_images WHERE gto_id = ? AND image_id = ?',
'DELETE FROM gto_images WHERE gto_id = $1 AND image_id = $2',
[req.params.id, req.params.imageId]
);
res.json({ success: true });
+32 -28
View File
@@ -14,17 +14,19 @@ router.get('/', async (req, res) => {
const search = req.query.search || '';
const clientName = req.query.client || '';
let query = `SELECT * FROM images WHERE enabled = ?`;
let query = `SELECT * FROM images WHERE enabled = $1`;
let params = [showDisabled ? 0 : 1];
let paramIndex = 2;
if (clientName) {
query += ` AND client_name = ?`;
query += ` AND client_name = $${paramIndex++}`;
params.push(clientName);
}
if (search) {
query += ` AND (patient_name LIKE ? OR image_guid LIKE ? OR filename LIKE ?)`;
query += ` AND (patient_name LIKE $${paramIndex} OR image_guid LIKE $${paramIndex + 1} OR filename LIKE $${paramIndex + 2})`;
params.push(`%${search}%`, `%${search}%`, `%${search}%`);
paramIndex += 3;
}
query += ` ORDER BY created_at DESC LIMIT 100`;
@@ -47,17 +49,19 @@ router.get('/patients', async (req, res) => {
const clientName = req.query.client || '';
const enabledVal = showDisabled ? 0 : 1;
let where = `WHERE enabled = ${enabledVal}`;
let params = [];
let paramIndex = 1;
let where = `WHERE enabled = $${paramIndex++}`;
let params = [enabledVal];
if (clientName) {
where += ` AND client_name = ?`;
where += ` AND client_name = $${paramIndex++}`;
params.push(clientName);
}
if (search) {
where += ` AND (patient_name LIKE ? OR image_guid LIKE ?)`;
where += ` AND (patient_name LIKE $${paramIndex} OR image_guid LIKE $${paramIndex + 1})`;
params.push(`%${search}%`, `%${search}%`);
paramIndex += 2;
}
// Group by patient_name — pick thumbnail from most recent image
@@ -102,7 +106,7 @@ router.get('/by-patient', async (req, res) => {
const images = await db.all(
`SELECT * FROM images
WHERE patient_name = ? AND enabled = ?
WHERE patient_name = $1 AND enabled = $2
ORDER BY created_at DESC`,
[patientName, showDisabled ? 0 : 1]
);
@@ -119,7 +123,7 @@ router.get('/by-patient', async (req, res) => {
// ================================================================
router.get('/:id', async (req, res) => {
try {
const image = await db.get('SELECT * FROM images WHERE id = ?', [req.params.id]);
const image = await db.get('SELECT * FROM images WHERE id = $1', [req.params.id]);
if (!image) {
return res.status(404).json({ error: 'Imagem não encontrada' });
}
@@ -155,14 +159,14 @@ router.get('/stats', async (req, res) => {
// ================================================================
router.get('/:id/transformations', async (req, res) => {
try {
const image = await db.get('SELECT * FROM images WHERE id = ?', [req.params.id]);
const image = await db.get('SELECT * FROM images WHERE id = $1', [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]);
const originalImage = await db.get('SELECT * FROM images WHERE id = $1', [originalId]);
const originalPath = path.join(__dirname, '../uploads', originalImage.filename);
let originalBuffer;
@@ -217,7 +221,7 @@ router.get('/:id/transformations', async (req, res) => {
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]);
const originalImage = await db.get('SELECT * FROM images WHERE id = $1', [req.params.id]);
if (!originalImage) {
return res.status(404).json({ error: 'Imagem não encontrada' });
@@ -225,7 +229,7 @@ router.post('/:id/transform', async (req, res) => {
// 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]);
const realOriginal = await db.get('SELECT * FROM images WHERE id = $1', [realOriginalId]);
// Processar a imagem
const originalPath = path.join(__dirname, '../uploads', realOriginal.filename);
@@ -250,9 +254,9 @@ router.post('/:id/transform', async (req, res) => {
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]);
// 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]);
const newRemark = remark !== undefined ? remark : originalImage.remark;
// Inserir a nova imagem como a versão ativa
@@ -260,7 +264,7 @@ router.post('/:id/transform', async (req, res) => {
`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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)`,
[
`${realOriginal.image_guid}_${timestamp}`,
realOriginal.patient_name,
@@ -298,11 +302,11 @@ router.put('/:id/patient-info', async (req, res) => {
await db.run(
`UPDATE images SET
patient_name_resumed = ?,
tooth_number = ?,
tooth_side = ?,
patient_name_resumed = $1,
tooth_number = $2,
tooth_side = $3,
enabled = 1
WHERE id = ?`,
WHERE id = $4`,
[patientNameResumed, toothNumber, toothSide, req.params.id]
);
@@ -317,7 +321,7 @@ router.put('/:id/patient-info', async (req, res) => {
// ================================================================
router.get('/:id/download', async (req, res) => {
try {
const image = await db.get('SELECT * FROM images WHERE id = ?', [req.params.id]);
const image = await db.get('SELECT * FROM images WHERE id = $1', [req.params.id]);
if (!image) {
return res.status(404).json({ error: 'Imagem não encontrada' });
}
@@ -344,7 +348,7 @@ router.get('/:id/download', async (req, res) => {
// ================================================================
router.delete('/:id', async (req, res) => {
try {
const image = await db.get('SELECT * FROM images WHERE id = ?', [req.params.id]);
const image = await db.get('SELECT * FROM images WHERE id = $1', [req.params.id]);
if (!image) {
return res.status(404).json({ error: 'Imagem não encontrada' });
}
@@ -366,7 +370,7 @@ router.delete('/:id', async (req, res) => {
}
// Apagar do banco
await db.run('DELETE FROM images WHERE id = ?', [req.params.id]);
await db.run('DELETE FROM images WHERE id = $1', [req.params.id]);
res.json({ success: true, message: 'Imagem apagada com sucesso' });
} catch (error) {
@@ -380,13 +384,13 @@ router.delete('/:id', async (req, res) => {
// ================================================================
router.put('/:id/toggle-enabled', async (req, res) => {
try {
const image = await db.get('SELECT * FROM images WHERE id = ?', [req.params.id]);
const image = await db.get('SELECT * FROM images WHERE id = $1', [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]);
await db.run('UPDATE images SET enabled = $1 WHERE id = $2', [newEnabled, req.params.id]);
res.json({ success: true, enabled: newEnabled === 1 });
} catch (error) {
-119
View File
@@ -1,119 +0,0 @@
const express = require('express');
const router = express.Router();
// Caminho do banco de dados oficial do Dental Sensor
const DENTAL_DB_PATH = 'C:/ProgramData/RF/Dental Sensor/data';
// ================================================================
// Conexão LAZY — abre o banco só quando necessário e fecha logo
// após, evitando conflitos de lock com o banco principal do servidor
// ================================================================
function openDentalDb() {
const { DatabaseSync } = require('node:sqlite');
return new DatabaseSync(DENTAL_DB_PATH);
}
// ================================================================
// POST /api/patients - Criar novo paciente no Dental Sensor
// ================================================================
router.post('/', (req, res) => {
let db;
try {
db = openDentalDb();
} catch (error) {
console.error('❌ Erro ao conectar ao banco do Dental Sensor:', error.message);
return res.status(500).json({
error: 'Não foi possível conectar ao banco do Dental Sensor. Verifique se o software está instalado.',
detail: error.message
});
}
const { firstName, lastName, doctor, birthday, gender, phone, email, address, zipCode, remark } = req.body;
if (!firstName || !lastName) {
if (db) try { db.close(); } catch (_) {}
return res.status(400).json({ error: 'Nome e Sobrenome são obrigatórios.' });
}
const formattedBirthday = birthday ? `${birthday} 00:00:00` : null;
try {
const insertStmt = db.prepare(`
INSERT INTO Patients (
FirstName, LastName, Doctor, Birthday, Gender, Phone, Email, Address, ZipCode, Remark, LastActTime, VFlag
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), 1)
`);
const result = insertStmt.run(
firstName,
lastName,
doctor || '',
formattedBirthday,
gender !== undefined ? parseInt(gender, 10) : 0,
phone || '',
email || '',
address || '',
zipCode || '',
remark || ''
);
res.status(201).json({
success: true,
patientId: result.lastInsertRowid,
message: 'Paciente cadastrado com sucesso no Dental Sensor'
});
} catch (error) {
console.error('Erro ao inserir paciente:', error);
res.status(500).json({ error: 'Erro ao cadastrar paciente: ' + error.message });
} finally {
// SEMPRE fechar o banco após o uso para liberar o lock
if (db) {
try { db.close(); } catch (_) {}
}
}
});
// ================================================================
// GET /api/patients - Listar pacientes do Dental Sensor (opcional)
// ================================================================
router.get('/', (req, res) => {
let db;
try {
db = openDentalDb();
const search = req.query.search || '';
let stmt;
let rows;
if (search) {
stmt = db.prepare(`
SELECT ID, FirstName, LastName, Doctor, Birthday, Gender, Phone, Remark
FROM Patients
WHERE (FirstName LIKE ? OR LastName LIKE ?)
AND VFlag = 1
ORDER BY LastActTime DESC
LIMIT 100
`);
rows = stmt.all(`%${search}%`, `%${search}%`);
} else {
stmt = db.prepare(`
SELECT ID, FirstName, LastName, Doctor, Birthday, Gender, Phone, Remark
FROM Patients
WHERE VFlag = 1
ORDER BY LastActTime DESC
LIMIT 100
`);
rows = stmt.all();
}
res.json(rows);
} catch (error) {
console.error('Erro ao listar pacientes do Dental Sensor:', error.message);
res.status(500).json({ error: 'Erro ao consultar banco do Dental Sensor: ' + error.message });
} finally {
if (db) {
try { db.close(); } catch (_) {}
}
}
});
module.exports = router;
+1 -1
View File
@@ -18,7 +18,7 @@ router.delete('/factory-reset', async (req, res) => {
}
// 1. Validar usuário e senha
const user = await db.get('SELECT * FROM users WHERE id = ?', [userId]);
const user = await db.get('SELECT * FROM users WHERE id = $1', [userId]);
if (!user) {
return res.status(404).json({ error: 'Usuário não encontrado' });
}