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
File diff suppressed because it is too large Load Diff
-8
View File
@@ -1,8 +0,0 @@
{
"serverUrl": "https://rx.scoreodonto.com",
"monitorPath": "C:\\ProgramData\\RF\\Dental Sensor\\Images",
"clientName": "COMPUTADOR-LOCAL",
"clientType": "windows",
"apiKey": "rf-dental-secure-key-2026",
"adminPassword": "admin"
}
-2628
View File
File diff suppressed because it is too large Load Diff
-13
View File
@@ -1,13 +0,0 @@
{
"name": "dental-client",
"version": "1.0.0",
"description": "Cliente Windows para envio de imagens dentais",
"main": "client-monitor.js",
"scripts": {
"start": "node client-monitor.js"
},
"dependencies": {
"socket.io-client": "^4.7.5",
"chokidar": "^3.5.3"
}
}
File diff suppressed because it is too large Load Diff
Binary file not shown.
-153
View File
@@ -1,153 +0,0 @@
# ✅ CONVERSÃO SQLite → MySQL COMPLETA
## 📋 RESUMO DA CONVERSÃO
Todas as referências a SQLite foram convertidas para MySQL.
---
## ✅ ARQUIVOS MODIFICADOS
### **1. package.json** ✅
- ❌ Removido: `sqlite3`
- ✅ Adicionado: `mysql2`
### **2. database.js** ✅
- ✅ Convertido de SQLite3 para MySQL2
- ✅ Pool de conexões implementado
- ✅ Sintaxe MySQL aplicada
- ✅ Tabelas criadas corretamente
- ✅ Índices aplicados
### **3. migration-v2.sql** ✅
- ✅ Sintaxe SQLite → MySQL
- ✅ CREATE TABLE IF NOT EXISTS
- ✅ AUTO_INCREMENT no lugar de INTEGER PRIMARY KEY
- ✅ TINYINT(1) no lugar de INTEGER
- ✅ VARCHAR no lugar de TEXT (onde apropriado)
- ✅ Índices criados
- ⚠️ Migration complexa criada (com verificações)
### **4. migration-simple.sql** ✅ (NOVO)
- ✅ Migration simplificada
- ✅ Recomendada para novo banco
- ✅ Cria tabelas completas de uma vez
### **5. ENV-CONFIG.txt** ✅
- ✅ Variáveis SQLite removidas
- ✅ Variáveis MySQL adicionadas:
- `DB_HOST`
- `DB_PORT`
- `DB_USER`
- `DB_PASSWORD`
- `DB_NAME`
### **6. INSTALACAO-DENTAL-SERVER.md** ✅
- ✅ Comandos SQLite → MySQL
- ✅ Instruções de migration atualizadas
- ✅ Troubleshooting atualizado
### **7. README.md** ✅
- ✅ Referências atualizadas
- ✅ Tecnologias atualizadas
### **8. INSTRUCOES-RAPIDAS.txt** ✅
- ✅ Variáveis de ambiente atualizadas
- ✅ Comandos de migration atualizados
---
## 🎯 PRINCIPAIS MUDANÇAS
### **Tipos de Dados**
| SQLite | MySQL |
|--------|-------|
| INTEGER PRIMARY KEY | INT AUTO_INCREMENT PRIMARY KEY |
| INTEGER DEFAULT 1 | TINYINT(1) DEFAULT 1 |
| TEXT | VARCHAR(255) ou TEXT |
| DATETIME DEFAULT CURRENT_TIMESTAMP | DATETIME DEFAULT CURRENT_TIMESTAMP ✅ |
### **Queries**
| SQLite | MySQL |
|--------|-------|
| `db.get()` | `pool.query()` |
| `db.all()` | `pool.query()` |
| `db.run()` | `pool.query()` |
| `this.lastID` | `result.insertId` |
| `this.changes` | `result.affectedRows` |
### **Connection**
| SQLite | MySQL |
|--------|-------|
| `sqlite3.Database(path)` | `mysql.createPool(config)` |
| Direto | Pool de conexões |
---
## 🚀 INSTALAÇÃO
### **1. Criar Banco**
```bash
mysql -u root -p -e "CREATE DATABASE dental_images CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
```
### **2. Executar Migration**
```bash
mysql -u root -p dental_images < migration-simple.sql
```
### **3. Configurar .env**
```env
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASSWORD=sua_senha
DB_NAME=dental_images
```
### **4. Instalar Dependências**
```bash
npm install
```
---
## ✅ FUNCIONALIDADES TESTADAS
- ✅ Conexão ao MySQL
- ✅ Criação de tabelas
- ✅ Criação de índices
- ✅ Queries básicas (get, all, run)
- ✅ Auto-increment
- ✅ Datetime defaults
- ✅ Pool de conexões
---
## 📦 DIFERENÇAS IMPORTANTES
### **Vantagens do MySQL:**
- ✅ Pool de conexões (melhor performance)
- ✅ Suporte a múltiplos usuários
- ✅ Melhor para produção
- ✅ Ferramentas de administração (phpMyAdmin, Workbench)
### **Desvantagens:**
- ⚠️ Requer servidor MySQL rodando
- ⚠️ Configuração mais complexa
- ⚠️ Precisa de credenciais
---
## 🎉 CONCLUSÃO
**Conversão 100% completa!**
Todos os arquivos foram atualizados para MySQL. O sistema está pronto para usar com banco MySQL.
---
**Status:** ✅ CONVERSÃO COMPLETA
**Banco:** MySQL
**Versão:** 2.0.0
**Data:** 2025-01-26
-75
View File
@@ -1,75 +0,0 @@
═══════════════════════════════════════════════════════════
✅ CONVERSÃO SQLite → MySQL COMPLETA
═══════════════════════════════════════════════════════════
🎉 TODOS OS ARQUIVOS FORAM CONVERTIDOS PARA MySQL!
═══════════════════════════════════════════════════════════
✅ O QUE FOI MUDADO
═══════════════════════════════════════════════════════════
1. package.json
❌ Removido: sqlite3
✅ Adicionado: mysql2
2. database.js
✅ Pool de conexões MySQL
✅ Sintaxe MySQL aplicada
✅ Funções convertidas
3. migration-simple.sql (NOVO)
✅ Migration MySQL completa
✅ Criar banco e tabelas
4. ENV-CONFIG.txt
✅ Variáveis MySQL adicionadas
5. Documentação
✅ INSTALACAO-DENTAL-SERVER.md atualizado
✅ README.md atualizado
✅ INSTRUCOES-RAPIDAS.txt atualizado
═══════════════════════════════════════════════════════════
🚀 COMO USAR COM MYSQL
═══════════════════════════════════════════════════════════
1. CRIAR BANCO:
mysql -u root -p
CREATE DATABASE dental_images;
exit
2. EXECUTAR MIGRATION:
mysql -u root -p dental_images < migration-simple.sql
3. CONFIGURAR .env:
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASSWORD=sua_senha
DB_NAME=dental_images
4. INSTALAR:
npm install
5. INICIAR:
pm2 start server.js --name dental-server
═══════════════════════════════════════════════════════════
✅ FUNCIONALIDADES TESTADAS
═══════════════════════════════════════════════════════════
✅ Conexão MySQL
✅ Criação de tabelas
✅ Índices
✅ Queries (get, all, run)
✅ Auto-increment
✅ Pool de conexões
✅ Zero erros de linter
═══════════════════════════════════════════════════════════
Status: ✅ CONVERSÃO COMPLETA
Banco: MySQL
Versão: 2.0.0
═══════════════════════════════════════════════════════════
+2 -2
View File
@@ -83,7 +83,7 @@ function generateToken(user, expiresIn = JWT_EXPIRES_IN) {
async function createInitialUser() { async function createInitialUser() {
try { try {
// Verificar se usuário já existe // Verificar se usuário já existe
const existingUser = await db.get('SELECT * FROM users WHERE username = ?', ['rcesar']); const existingUser = await db.get('SELECT * FROM users WHERE username = $1', ['rcesar']);
if (existingUser) { if (existingUser) {
console.log('👤 Usuário admin já existe:', existingUser.username); console.log('👤 Usuário admin já existe:', existingUser.username);
@@ -96,7 +96,7 @@ async function createInitialUser() {
// Inserir usuário // Inserir usuário
await db.run( await db.run(
`INSERT INTO users (username, email, password_hash, is_admin, created_at) `INSERT INTO users (username, email, password_hash, is_admin, created_at)
VALUES (?, ?, ?, 1, NOW())`, VALUES ($1, $2, $3, true, CURRENT_TIMESTAMP) RETURNING id`,
['rcesar', 'rcesar@rcesar.com', passwordHash] ['rcesar', 'rcesar@rcesar.com', passwordHash]
); );
+1 -1
View File
@@ -44,7 +44,7 @@ async function runBackfill() {
await storage.saveImage(thumbFilename, thumbBuffer, img.client_name, img.patient_name, false); await storage.saveImage(thumbFilename, thumbBuffer, img.client_name, img.patient_name, false);
// d. Atualizar no banco de dados // d. Atualizar no banco de dados
await db.run('UPDATE images SET thumb_filename = ? WHERE id = ?', [thumbFilename, img.id]); await db.run('UPDATE images SET thumb_filename = $1 WHERE id = $2', [thumbFilename, img.id]);
console.log(`✅ [ID ${img.id}] Thumbnail salvo com sucesso: ${thumbFilename} (Paciente: ${img.patient_name})`); console.log(`✅ [ID ${img.id}] Thumbnail salvo com sucesso: ${thumbFilename} (Paciente: ${img.patient_name})`);
successCount++; successCount++;
+4 -39
View File
@@ -1,42 +1,8 @@
const { Pool } = require('pg'); const { Pool } = require('pg');
require('dotenv').config();
let pool = null; let pool = null;
// ================================================================
// FUNÇÕES AUXILIARES DE COMPATIBILIDADE (MYSQL -> POSTGRES)
// ================================================================
function rewriteToPostgres(sql) {
let rewritten = sql;
// 1. Converter MySQL NOW() para Postgres CURRENT_TIMESTAMP
rewritten = rewritten.replace(/\bNOW\(\)/gi, 'CURRENT_TIMESTAMP');
// 2. Converter parâmetros de ? para $1, $2, etc.
let paramIndex = 1;
rewritten = rewritten.replace(/\?/g, () => `$${paramIndex++}`);
// 3. Adicionar RETURNING id nas inserções se não houver
if (rewritten.trim().toUpperCase().startsWith('INSERT INTO')) {
if (!rewritten.toUpperCase().includes('RETURNING')) {
rewritten += ' RETURNING id';
}
}
// 4. Reescrever ON DUPLICATE KEY UPDATE do MySQL
if (rewritten.includes('ON DUPLICATE KEY UPDATE')) {
// Isso é específico para a query de inserção de usuário default
rewritten = `INSERT INTO users (username, email, password_hash, created_at)
VALUES ($1, $2, $3, CURRENT_TIMESTAMP)
ON CONFLICT (username) DO UPDATE SET
email = EXCLUDED.email,
password_hash = EXCLUDED.password_hash
RETURNING id`;
}
return rewritten;
}
// ================================================================ // ================================================================
// INICIALIZAR BANCO DE DADOS POSTGRESQL // INICIALIZAR BANCO DE DADOS POSTGRESQL
// ================================================================ // ================================================================
@@ -218,7 +184,7 @@ async function createTables() {
function get(query, params = []) { function get(query, params = []) {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
try { try {
const res = await pool.query(rewriteToPostgres(query), params); const res = await pool.query(query, params);
resolve(res.rows[0] || null); resolve(res.rows[0] || null);
} catch (error) { } catch (error) {
reject(error); reject(error);
@@ -229,7 +195,7 @@ function get(query, params = []) {
function all(query, params = []) { function all(query, params = []) {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
try { try {
const res = await pool.query(rewriteToPostgres(query), params); const res = await pool.query(query, params);
resolve(res.rows || []); resolve(res.rows || []);
} catch (error) { } catch (error) {
reject(error); reject(error);
@@ -240,8 +206,7 @@ function all(query, params = []) {
function run(query, params = []) { function run(query, params = []) {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
try { try {
const rewritten = rewriteToPostgres(query); const res = await pool.query(query, params);
const res = await pool.query(rewritten, params);
// No PostgreSQL, se adicionamos RETURNING id, o id estará no res.rows[0] // No PostgreSQL, se adicionamos RETURNING id, o id estará no res.rows[0]
const insertedId = (res.rows && res.rows[0] && res.rows[0].id) ? res.rows[0].id : null; const insertedId = (res.rows && res.rows[0] && res.rows[0].id) ? res.rows[0].id : null;
@@ -43,7 +43,7 @@ async function checkInstallation() {
} else { } else {
// Tentar inicializar se .env tem configurações // Tentar inicializar se .env tem configurações
const envContent = fs.readFileSync(envPath, 'utf8'); const envContent = fs.readFileSync(envPath, 'utf8');
if (envContent.includes('DB_TYPE=sqlite') || (envContent.includes('DB_HOST') && envContent.includes('DB_NAME'))) { if (envContent.includes('DB_HOST') && envContent.includes('DB_NAME')) {
// Recarregar .env // Recarregar .env
require('dotenv').config({ path: envPath }); require('dotenv').config({ path: envPath });
-104
View File
@@ -1,104 +0,0 @@
/*M!999999\- enable the sandbox mode */
-- MariaDB dump 10.19 Distrib 10.11.10-MariaDB, for Linux (x86_64)
--
-- Host: localhost Database: dental_images
-- ------------------------------------------------------
-- Server version 10.11.10-MariaDB-log
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Table structure for table `images`
--
DROP TABLE IF EXISTS `images`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `images` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`image_guid` varchar(255) NOT NULL,
`patient_name` text DEFAULT NULL,
`client_name` varchar(100) DEFAULT NULL,
`original_filename` text DEFAULT NULL,
`filename` text NOT NULL,
`enabled` tinyint(1) DEFAULT 1,
`tooth_number` varchar(10) DEFAULT NULL,
`tooth_side` varchar(10) DEFAULT NULL,
`patient_name_resumed` varchar(50) DEFAULT NULL,
`original_image_id` int(11) DEFAULT NULL,
`rotation` int(11) DEFAULT 0,
`flip_horizontal` int(11) DEFAULT 0,
`flip_vertical` int(11) DEFAULT 0,
`created_at` datetime DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
KEY `idx_images_enabled` (`enabled`),
KEY `idx_images_original` (`original_image_id`),
KEY `idx_patient_name` (`patient_name`(100)),
KEY `idx_image_guid` (`image_guid`),
KEY `idx_created_at` (`created_at`),
KEY `idx_client_name` (`client_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `images`
--
LOCK TABLES `images` WRITE;
/*!40000 ALTER TABLE `images` DISABLE KEYS */;
/*!40000 ALTER TABLE `images` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `users`
--
DROP TABLE IF EXISTS `users`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`email` varchar(100) NOT NULL,
`password_hash` text NOT NULL,
`created_at` datetime DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`),
UNIQUE KEY `email` (`email`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `users`
--
LOCK TABLES `users` WRITE;
/*!40000 ALTER TABLE `users` DISABLE KEYS */;
INSERT INTO `users` VALUES
(1,'rcesar','rcesar@rcesar.com','$2b$10$i0zu9BGACsu6TDTsrU1EaOGZkKnvp8zwjOek1W/3m9mP4mzbN8HkC','2025-10-31 07:16:50');
/*!40000 ALTER TABLE `users` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Dumping routines for database 'dental_images'
--
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2025-10-31 21:24:39
-84
View File
@@ -1,84 +0,0 @@
/*M!999999\- enable the sandbox mode */
-- MariaDB dump 10.19 Distrib 10.11.10-MariaDB, for Linux (x86_64)
--
-- Host: localhost Database: dental_images
-- ------------------------------------------------------
-- Server version 10.11.10-MariaDB-log
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Table structure for table `images`
--
DROP TABLE IF EXISTS `images`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `images` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`image_guid` varchar(255) NOT NULL,
`patient_name` text DEFAULT NULL,
`client_name` varchar(100) DEFAULT NULL,
`original_filename` text DEFAULT NULL,
`filename` text NOT NULL,
`enabled` tinyint(1) DEFAULT 1,
`tooth_number` varchar(10) DEFAULT NULL,
`tooth_side` varchar(10) DEFAULT NULL,
`patient_name_resumed` varchar(50) DEFAULT NULL,
`original_image_id` int(11) DEFAULT NULL,
`rotation` int(11) DEFAULT 0,
`flip_horizontal` int(11) DEFAULT 0,
`flip_vertical` int(11) DEFAULT 0,
`created_at` datetime DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
KEY `idx_images_enabled` (`enabled`),
KEY `idx_images_original` (`original_image_id`),
KEY `idx_patient_name` (`patient_name`(100)),
KEY `idx_image_guid` (`image_guid`),
KEY `idx_created_at` (`created_at`),
KEY `idx_client_name` (`client_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `users`
--
DROP TABLE IF EXISTS `users`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`email` varchar(100) NOT NULL,
`password_hash` text NOT NULL,
`created_at` datetime DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`),
UNIQUE KEY `email` (`email`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping routines for database 'dental_images'
--
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2025-10-31 21:24:36
+43 -3
View File
@@ -5,6 +5,20 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gerenciamento de Clínicas - RF Dental</title> <title>Gerenciamento de Clínicas - RF Dental</title>
<script>
// Redirecionar se não estiver autenticado ou não for administrador
if (!localStorage.getItem('auth_token')) {
window.location.replace('/login');
} else if (localStorage.getItem('is_admin') !== 'true') {
window.location.replace('/');
} else {
// Adiciona classe de admin no body para que os estilos de admin sejam exibidos
document.addEventListener('DOMContentLoaded', () => {
document.body.classList.add('is-admin');
});
}
</script>
<!-- Fontes --> <!-- Fontes -->
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
@@ -28,15 +42,41 @@
</div> </div>
<nav class="sidebar-nav"> <nav class="sidebar-nav">
<a href="/" class="nav-item"> <div class="nav-section-title">Menu Principal</div>
<a href="/" class="nav-item" id="menu-dashboard">
<i class="fa-solid fa-chart-line"></i> Dashboard <i class="fa-solid fa-chart-line"></i> Dashboard
</a> </a>
<a href="/clients" class="nav-item"> <a href="/?action=new-patient" class="nav-item" id="menu-new-patient" onclick="if(window.location.pathname === '/') { openCreatePatientModal(); return false; }">
<i class="fa-solid fa-user-plus"></i> Novo Paciente
</a>
<a href="/?action=sync-upload" class="nav-item" id="menu-upload" onclick="if(window.location.pathname === '/') { openSyncModal('upload'); return false; }">
<i class="fa-solid fa-upload"></i> Enviar Imagens
</a>
<a href="/?action=settings" class="nav-item" id="menu-settings" onclick="if(window.location.pathname === '/') { openSettingsModal(); return false; }">
<i class="fa-solid fa-user-gear"></i> Minha Conta
</a>
<div class="admin-only">
<div class="nav-section-title">Área Administrativa</div>
<a href="/clients" class="nav-item" id="menu-clients">
<i class="fa-solid fa-network-wired"></i> Conexões <i class="fa-solid fa-network-wired"></i> Conexões
</a> </a>
<a href="/admin-clinics" class="nav-item active"> <a href="/admin-clinics" class="nav-item active" id="menu-clinics">
<i class="fa-solid fa-hospital"></i> Clínicas e Acessos <i class="fa-solid fa-hospital"></i> Clínicas e Acessos
</a> </a>
<a href="/?action=users" class="nav-item" id="menu-users" onclick="if(window.location.pathname === '/') { openUserManagementModal(); return false; }">
<i class="fa-solid fa-users"></i> Gerenciar Usuários
</a>
<a href="/?action=sync-devices" class="nav-item" id="menu-sync" onclick="if(window.location.pathname === '/') { openSyncModal('devices'); return false; }">
<i class="fa-solid fa-rotate"></i> Sincronização
</a>
<a href="/?action=plugins" class="nav-item" id="menu-plugins" onclick="if(window.location.pathname === '/') { openPluginsModal(); return false; }">
<i class="fa-solid fa-plug"></i> Plugins / Wasabi
</a>
<a href="/reset" class="nav-item" id="menu-reset" style="color: #dc2626;">
<i class="fa-solid fa-triangle-exclamation"></i> Zerar Sistema
</a>
</div>
</nav> </nav>
<div class="sidebar-footer"> <div class="sidebar-footer">
+7
View File
@@ -4,6 +4,7 @@ document.addEventListener('DOMContentLoaded', () => {
// Pegar Token JWT do Admin // Pegar Token JWT do Admin
const token = localStorage.getItem('auth_token') || ''; const token = localStorage.getItem('auth_token') || '';
const isAdmin = localStorage.getItem('is_admin') === 'true';
// Se não tem token, redirecionar imediatamente para login // Se não tem token, redirecionar imediatamente para login
if (!token) { if (!token) {
@@ -11,6 +12,12 @@ document.addEventListener('DOMContentLoaded', () => {
return; return;
} }
// Se não for administrador, redirecionar imediatamente para a home
if (!isAdmin) {
window.location.href = '/';
return;
}
const fetchOptions = { const fetchOptions = {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
+13 -12
View File
@@ -78,15 +78,11 @@ async function verifyAuth() {
} }
function updateAdminMenuVisibility(isAdmin) { function updateAdminMenuVisibility(isAdmin) {
// Esconder links não autorizados da Sidebar if (isAdmin) {
const connectionsLink = document.querySelector('.sidebar-nav .nav-item[href="/clients"]'); document.body.classList.add('is-admin');
const clinicsLink = document.querySelector('.sidebar-nav .nav-item[href="/admin-clinics"]'); } else {
if (connectionsLink) connectionsLink.style.display = isAdmin ? 'flex' : 'none'; document.body.classList.remove('is-admin');
if (clinicsLink) clinicsLink.style.display = isAdmin ? 'flex' : 'none'; }
// Esconder botão de gerenciamento de usuários na toolbar
const btnUserManagement = document.getElementById('btnUserManagement');
if (btnUserManagement) btnUserManagement.style.display = isAdmin ? 'flex' : 'none';
} }
async function fetchWithAuth(url, options = {}) { async function fetchWithAuth(url, options = {}) {
@@ -189,7 +185,10 @@ document.addEventListener('DOMContentLoaded', async () => {
if (action === 'new-patient') window.openCreatePatientModal(); if (action === 'new-patient') window.openCreatePatientModal();
else if (action === 'settings') window.openSettingsModal(); else if (action === 'settings') window.openSettingsModal();
else if (action === 'plugins') window.openPluginsModal(); else if (action === 'plugins') window.openPluginsModal();
else if (action === 'sync') window.openSyncModal(); else if (action === 'sync-devices') window.openSyncModal('devices');
else if (action === 'sync-upload') window.openSyncModal('upload');
else if (action === 'sync') window.openSyncModal('devices');
else if (action === 'users') window.openUserManagementModal();
// Remove querystring to prevent reopening on reload // Remove querystring to prevent reopening on reload
window.history.replaceState({}, document.title, window.location.pathname); window.history.replaceState({}, document.title, window.location.pathname);
@@ -1505,7 +1504,7 @@ window.triggerSync = triggerSync;
let selectedUploadFiles = []; let selectedUploadFiles = [];
function openSyncModal() { function openSyncModal(defaultTab = 'devices') {
const modal = document.getElementById('syncModal'); const modal = document.getElementById('syncModal');
if (!modal) return; if (!modal) return;
@@ -1513,13 +1512,15 @@ function openSyncModal() {
modal.style.display = 'flex'; modal.style.display = 'flex';
// Resetar abas // Resetar abas
switchSyncTab('devices'); switchSyncTab(defaultTab);
// Limpar formulário de upload // Limpar formulário de upload
resetManualUploadForm(); resetManualUploadForm();
// Carregar dados // Carregar dados
if (defaultTab === 'devices') {
loadSyncDevices(); loadSyncDevices();
}
refreshUploadPatients(); refreshUploadPatients();
// Escuta de clique fora para fechar modal // Escuta de clique fora para fechar modal
+38 -5
View File
@@ -179,9 +179,16 @@
</style> </style>
<script> <script>
// Redirecionar se não estiver autenticado // Redirecionar se não estiver autenticado ou não for administrador
if (!localStorage.getItem('auth_token')) { if (!localStorage.getItem('auth_token')) {
window.location.href = '/login'; window.location.replace('/login');
} else if (localStorage.getItem('is_admin') !== 'true') {
window.location.replace('/');
} else {
// Adiciona classe de admin no body para que os estilos de admin sejam exibidos
document.addEventListener('DOMContentLoaded', () => {
document.body.classList.add('is-admin');
});
} }
</script> </script>
</head> </head>
@@ -196,15 +203,41 @@
</div> </div>
<nav class="sidebar-nav"> <nav class="sidebar-nav">
<a href="/" class="nav-item"> <div class="nav-section-title">Menu Principal</div>
<a href="/" class="nav-item" id="menu-dashboard">
<i class="fa-solid fa-chart-line"></i> Dashboard <i class="fa-solid fa-chart-line"></i> Dashboard
</a> </a>
<a href="/clients" class="nav-item active"> <a href="/?action=new-patient" class="nav-item" id="menu-new-patient" onclick="if(window.location.pathname === '/') { openCreatePatientModal(); return false; }">
<i class="fa-solid fa-user-plus"></i> Novo Paciente
</a>
<a href="/?action=sync-upload" class="nav-item" id="menu-upload" onclick="if(window.location.pathname === '/') { openSyncModal('upload'); return false; }">
<i class="fa-solid fa-upload"></i> Enviar Imagens
</a>
<a href="/?action=settings" class="nav-item" id="menu-settings" onclick="if(window.location.pathname === '/') { openSettingsModal(); return false; }">
<i class="fa-solid fa-user-gear"></i> Minha Conta
</a>
<div class="admin-only">
<div class="nav-section-title">Área Administrativa</div>
<a href="/clients" class="nav-item active" id="menu-clients">
<i class="fa-solid fa-network-wired"></i> Conexões <i class="fa-solid fa-network-wired"></i> Conexões
</a> </a>
<a href="/admin-clinics" class="nav-item"> <a href="/admin-clinics" class="nav-item" id="menu-clinics">
<i class="fa-solid fa-hospital"></i> Clínicas e Acessos <i class="fa-solid fa-hospital"></i> Clínicas e Acessos
</a> </a>
<a href="/?action=users" class="nav-item" id="menu-users" onclick="if(window.location.pathname === '/') { openUserManagementModal(); return false; }">
<i class="fa-solid fa-users"></i> Gerenciar Usuários
</a>
<a href="/?action=sync-devices" class="nav-item" id="menu-sync" onclick="if(window.location.pathname === '/') { openSyncModal('devices'); return false; }">
<i class="fa-solid fa-rotate"></i> Sincronização
</a>
<a href="/?action=plugins" class="nav-item" id="menu-plugins" onclick="if(window.location.pathname === '/') { openPluginsModal(); return false; }">
<i class="fa-solid fa-plug"></i> Plugins / Wasabi
</a>
<a href="/reset" class="nav-item" id="menu-reset" style="color: #dc2626;">
<i class="fa-solid fa-triangle-exclamation"></i> Zerar Sistema
</a>
</div>
</nav> </nav>
<div class="sidebar-footer"> <div class="sidebar-footer">
+31 -26
View File
@@ -21,15 +21,41 @@
</div> </div>
<nav class="sidebar-nav"> <nav class="sidebar-nav">
<a href="/" class="nav-item active"> <div class="nav-section-title">Menu Principal</div>
<a href="/" class="nav-item active" id="menu-dashboard" onclick="if(window.location.pathname === '/') { showView('gallery'); return false; }">
<i class="fa-solid fa-chart-line"></i> Dashboard <i class="fa-solid fa-chart-line"></i> Dashboard
</a> </a>
<a href="/clients" class="nav-item"> <a href="#" class="nav-item" id="menu-new-patient" onclick="openCreatePatientModal(); return false;">
<i class="fa-solid fa-user-plus"></i> Novo Paciente
</a>
<a href="#" class="nav-item" id="menu-upload" onclick="openSyncModal('upload'); return false;">
<i class="fa-solid fa-upload"></i> Enviar Imagens
</a>
<a href="#" class="nav-item" id="menu-settings" onclick="openSettingsModal(); return false;">
<i class="fa-solid fa-user-gear"></i> Minha Conta
</a>
<div class="admin-only">
<div class="nav-section-title">Área Administrativa</div>
<a href="/clients" class="nav-item" id="menu-clients">
<i class="fa-solid fa-network-wired"></i> Conexões <i class="fa-solid fa-network-wired"></i> Conexões
</a> </a>
<a href="/admin-clinics" class="nav-item"> <a href="/admin-clinics" class="nav-item" id="menu-clinics">
<i class="fa-solid fa-hospital"></i> Clínicas e Acessos <i class="fa-solid fa-hospital"></i> Clínicas e Acessos
</a> </a>
<a href="#" class="nav-item" id="menu-users" onclick="openUserManagementModal(); return false;">
<i class="fa-solid fa-users"></i> Gerenciar Usuários
</a>
<a href="#" class="nav-item" id="menu-sync" onclick="openSyncModal('devices'); return false;">
<i class="fa-solid fa-rotate"></i> Sincronização
</a>
<a href="#" class="nav-item" id="menu-plugins" onclick="openPluginsModal(); return false;">
<i class="fa-solid fa-plug"></i> Plugins / Wasabi
</a>
<a href="/reset" class="nav-item" id="menu-reset" style="color: #dc2626;">
<i class="fa-solid fa-triangle-exclamation"></i> Zerar Sistema
</a>
</div>
</nav> </nav>
<div class="sidebar-footer"> <div class="sidebar-footer">
@@ -75,8 +101,8 @@
<!-- Scrollable content area --> <!-- Scrollable content area -->
<div class="content-scroll"> <div class="content-scroll">
<!-- Toolbar de Filtros e Ações Rápidas (Migradas da Sidebar) --> <!-- Toolbar de Filtros e Ações Rápidas -->
<div class="toolbar glass-card" style="margin-bottom: 24px; padding: 16px; display: flex; gap: 16px; align-items: center; justify-content: space-between; flex-wrap: wrap; background: rgba(255, 255, 255, 0.4); border: 1px solid rgba(255, 255, 255, 0.3); border-radius: 12px; box-shadow: var(--shadow-sm);"> <div class="toolbar glass-card" style="margin-bottom: 24px; padding: 16px; display: flex; gap: 16px; align-items: center; background: rgba(255, 255, 255, 0.4); border: 1px solid rgba(255, 255, 255, 0.3); border-radius: 12px; box-shadow: var(--shadow-sm);">
<div class="toolbar-left" style="display: flex; gap: 12px; align-items: center; flex-wrap: wrap;"> <div class="toolbar-left" style="display: flex; gap: 12px; align-items: center; flex-wrap: wrap;">
<!-- Filtro de Clientes --> <!-- Filtro de Clientes -->
<select id="clientFilter" class="client-filter" style="padding: 10px 16px; border-radius: 10px; border: 1px solid var(--border-color); background: var(--bg-surface); color: var(--text-primary); font-size: 0.95rem; font-weight: 500; outline: none; min-width: 200px; cursor: pointer;"> <select id="clientFilter" class="client-filter" style="padding: 10px 16px; border-radius: 10px; border: 1px solid var(--border-color); background: var(--bg-surface); color: var(--text-primary); font-size: 0.95rem; font-weight: 500; outline: none; min-width: 200px; cursor: pointer;">
@@ -87,27 +113,6 @@
👁️ <span id="toggleText">Ocultas</span> 👁️ <span id="toggleText">Ocultas</span>
</button> </button>
</div> </div>
<div class="toolbar-right" style="display: flex; gap: 10px; align-items: center; flex-wrap: wrap;">
<button class="btn btn-primary" onclick="openCreatePatientModal()" style="padding: 10px 16px; border-radius: 10px; font-weight: 600; font-size: 0.95rem; background: var(--primary-color); color: white; border: none; cursor: pointer; display: flex; align-items: center; gap: 6px; box-shadow: 0 4px 10px rgba(79, 70, 229, 0.2);">
Novo Paciente
</button>
<button id="btnUserManagement" class="btn btn-secondary" onclick="openUserManagementModal()" title="Gerenciar Usuários" style="display: none; padding: 10px 16px; border-radius: 10px; font-weight: 500; font-size: 0.95rem; border: 1px solid var(--border-color); background: var(--bg-surface); color: var(--text-primary); cursor: pointer; align-items: center; gap: 6px;">
👥 Gerenciar Usuários
</button>
<button class="btn btn-secondary" onclick="openSettingsModal()" title="Configurações" style="padding: 10px 16px; border-radius: 10px; font-weight: 500; font-size: 0.95rem; border: 1px solid var(--border-color); background: var(--bg-surface); color: var(--text-primary); cursor: pointer; display: flex; align-items: center; gap: 6px;">
⚙️ Configurações
</button>
<button class="btn btn-secondary" onclick="openPluginsModal()" title="Plugins / Wasabi" style="padding: 10px 16px; border-radius: 10px; font-weight: 500; font-size: 0.95rem; border: 1px solid var(--border-color); background: var(--bg-surface); color: var(--text-primary); cursor: pointer; display: flex; align-items: center; gap: 6px;">
🔌 Plugins / Wasabi
</button>
<button class="btn btn-secondary" onclick="openSyncModal()" title="Sincronização" style="padding: 10px 16px; border-radius: 10px; font-weight: 500; font-size: 0.95rem; border: 1px solid var(--border-color); background: var(--bg-surface); color: var(--text-primary); cursor: pointer; display: flex; align-items: center; gap: 6px;">
🔄 Sincronização
</button>
<a href="/reset" class="btn btn-danger" title="Zerar Sistema" style="padding: 10px 16px; border-radius: 10px; font-weight: 500; font-size: 0.95rem; background: #fee2e2; color: #dc2626; border: 1px solid #fecaca; text-decoration: none; cursor: pointer; display: inline-flex; align-items: center; gap: 6px;">
⚠️ Zerar Sistema
</a>
</div>
</div> </div>
<!-- Loading State --> <!-- Loading State -->
+38
View File
@@ -942,3 +942,41 @@ body {
max-height: 95vh; max-height: 95vh;
} }
} }
/* RF Dental Premium Sidebar Menus Additions */
.nav-section-title {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 1.2px;
font-weight: 700;
color: var(--text-secondary);
padding: 12px 16px 4px 16px;
opacity: 0.6;
}
/* Ocultação inicial de elementos administrativos */
.admin-only {
display: none !important;
}
/* Exibição quando autenticado como admin */
body.is-admin .admin-only {
display: flex !important;
flex-direction: column;
gap: 8px;
width: 100%;
}
/* Responsividade mobile para o agrupamento admin */
@media (max-width: 768px) {
.nav-section-title {
display: none !important;
}
body.is-admin .admin-only {
display: flex !important;
flex-direction: row !important;
gap: 16px !important;
width: auto;
}
}
+4 -4
View File
@@ -41,7 +41,7 @@ router.post('/', requireAdmin, async (req, res) => {
try { try {
// Verificar se email já existe // 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) { if (existing) {
return res.status(400).json({ success: false, message: 'Email já cadastrado para outra clínica' }); 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(` await db.run(`
INSERT INTO clinics_devices (clinic_name, email, password_hash, pc_name, machine_token) 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]); `, [clinic_name, email, password_hash, pc_name, machine_token]);
res.json({ res.json({
@@ -74,7 +74,7 @@ router.put('/:id/status', requireAdmin, async (req, res) => {
const { is_active } = req.body; const { is_active } = req.body;
try { 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' }); res.json({ success: true, message: 'Status atualizado com sucesso' });
} catch (error) { } catch (error) {
res.status(500).json({ success: false, message: 'Erro interno' }); res.status(500).json({ success: false, message: 'Erro interno' });
@@ -88,7 +88,7 @@ router.delete('/:id', requireAdmin, async (req, res) => {
const { id } = req.params; const { id } = req.params;
try { 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' }); res.json({ success: true, message: 'Clínica excluída com sucesso' });
} catch (error) { } catch (error) {
res.status(500).json({ success: false, message: 'Erro interno' }); 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 // Buscar usuário
const user = await db.get( const user = await db.get(
'SELECT * FROM users WHERE username = ? OR email = ?', 'SELECT * FROM users WHERE username = $1 OR email = $2',
[username, username] [username, username]
); );
@@ -77,7 +77,7 @@ router.get('/verify', async (req, res) => {
const jwt = require('jsonwebtoken'); const jwt = require('jsonwebtoken');
const decoded = jwt.verify(token, require('../auth').JWT_SECRET); 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) { if (!user) {
return res.status(404).json({ error: 'Usuário não encontrado' }); 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 // Verificar se usuário já existe
const existing = await db.get( const existing = await db.get(
'SELECT * FROM users WHERE username = ? OR email = ?', 'SELECT * FROM users WHERE username = $1 OR email = $2',
[username, email] [username, email]
); );
@@ -147,7 +147,7 @@ router.post('/create-user', authenticateToken, async (req, res) => {
// Criar usuário // Criar usuário
const result = await db.run( const result = await db.run(
`INSERT INTO users (username, email, password_hash, clinic_name, pc_name, is_admin, created_at) `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] [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) { if (parseInt(userId) === req.user.id) {
return res.status(400).json({ error: 'Não é possível excluir o próprio usuário admin' }); 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' }); res.json({ success: true, message: 'Usuário excluído com sucesso' });
} catch (error) { } catch (error) {
res.status(500).json({ error: 'Erro ao excluir usuário' }); 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) { if (!req.user.is_admin) {
return res.status(403).json({ error: 'Acesso negado' }); 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' }); 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) // 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 // 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) { if (!user) {
return res.status(404).json({ error: 'Usuário não encontrado' }); 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 // Verificar e aplicar novo username se fornecido
if (newUsername && newUsername !== user.username) { 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) { if (existing) {
return res.status(409).json({ error: 'Este nome de usuário já está em uso' }); 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 // Verificar e aplicar novo e-mail se fornecido
if (newEmail && newEmail !== user.email) { 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) { if (existing) {
return res.status(409).json({ error: 'Este e-mail já está em uso' }); 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 // Atualizar no banco de dados
await db.run( 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] [finalUsername, finalEmail, finalPasswordHash, userId]
); );
+2 -2
View File
@@ -22,7 +22,7 @@ router.post('/login', async (req, res) => {
try { try {
// 1. Buscar o dispositivo pelo Token // 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) { if (!device) {
return res.status(401).json({ success: false, message: 'Token da máquina inválido ou inexistente' }); 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 // Atualizar último IP conectado
const clientIp = req.headers['x-forwarded-for'] || req.socket.remoteAddress; 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 // 5. Gerar Token JWT de Sessão
// O Token vale por 2 horas, obrigando o cliente a re-logar ou pedir refresh // 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 `SELECT g.*, COUNT(gi.id) as image_count
FROM gtos g FROM gtos g
LEFT JOIN gto_images gi ON gi.gto_id = g.id LEFT JOIN gto_images gi ON gi.gto_id = g.id
WHERE g.patient_name = ? WHERE g.patient_name = $1
GROUP BY g.id GROUP BY g.id
ORDER BY g.created_at DESC`, ORDER BY g.created_at DESC`,
[patient] [patient]
@@ -42,14 +42,14 @@ router.get('/', async (req, res) => {
// ================================================================ // ================================================================
router.get('/:id', async (req, res) => { router.get('/:id', async (req, res) => {
try { 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' }); if (!gto) return res.status(404).json({ error: 'GTO não encontrada' });
const images = await db.all( const images = await db.all(
`SELECT i.*, gi.id as link_id `SELECT i.*, gi.id as link_id
FROM gto_images gi FROM gto_images gi
JOIN images i ON i.id = gi.image_id JOIN images i ON i.id = gi.image_id
WHERE gi.gto_id = ? WHERE gi.gto_id = $1
ORDER BY gi.created_at DESC`, ORDER BY gi.created_at DESC`,
[req.params.id] [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' }); return res.status(400).json({ error: 'gto_number e patient_name são obrigatórios' });
} }
const result = await db.run( 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 || ''] [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); res.status(201).json(gto);
} catch (err) { } catch (err) {
console.error('Erro ao criar GTO:', err); console.error('Erro ao criar GTO:', err);
@@ -89,10 +89,10 @@ router.put('/:id', async (req, res) => {
try { try {
const { gto_number, description } = req.body; const { gto_number, description } = req.body;
await db.run( 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] [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); res.json(gto);
} catch (err) { } catch (err) {
res.status(500).json({ error: err.message }); res.status(500).json({ error: err.message });
@@ -104,8 +104,8 @@ router.put('/:id', async (req, res) => {
// ================================================================ // ================================================================
router.delete('/:id', async (req, res) => { router.delete('/:id', async (req, res) => {
try { try {
await db.run('DELETE FROM gto_images WHERE gto_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 = ?', [req.params.id]); await db.run('DELETE FROM gtos WHERE id = $1', [req.params.id]);
res.json({ success: true }); res.json({ success: true });
} catch (err) { } catch (err) {
res.status(500).json({ error: err.message }); res.status(500).json({ error: err.message });
@@ -123,13 +123,13 @@ router.post('/:id/images', async (req, res) => {
// Verificar se já está vinculada // Verificar se já está vinculada
const existing = await db.get( 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] [req.params.id, image_id]
); );
if (existing) return res.status(409).json({ error: 'Imagem já vinculada a esta GTO' }); if (existing) return res.status(409).json({ error: 'Imagem já vinculada a esta GTO' });
await db.run( 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] [req.params.id, image_id]
); );
res.status(201).json({ success: true, message: 'Imagem vinculada à GTO' }); 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) => { router.delete('/:id/images/:imageId', async (req, res) => {
try { try {
await db.run( 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] [req.params.id, req.params.imageId]
); );
res.json({ success: true }); res.json({ success: true });
+29 -25
View File
@@ -14,17 +14,19 @@ router.get('/', async (req, res) => {
const search = req.query.search || ''; const search = req.query.search || '';
const clientName = req.query.client || ''; 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 params = [showDisabled ? 0 : 1];
let paramIndex = 2;
if (clientName) { if (clientName) {
query += ` AND client_name = ?`; query += ` AND client_name = $${paramIndex++}`;
params.push(clientName); params.push(clientName);
} }
if (search) { 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}%`); params.push(`%${search}%`, `%${search}%`, `%${search}%`);
paramIndex += 3;
} }
query += ` ORDER BY created_at DESC LIMIT 100`; query += ` ORDER BY created_at DESC LIMIT 100`;
@@ -47,17 +49,19 @@ router.get('/patients', async (req, res) => {
const clientName = req.query.client || ''; const clientName = req.query.client || '';
const enabledVal = showDisabled ? 0 : 1; const enabledVal = showDisabled ? 0 : 1;
let where = `WHERE enabled = ${enabledVal}`; let paramIndex = 1;
let params = []; let where = `WHERE enabled = $${paramIndex++}`;
let params = [enabledVal];
if (clientName) { if (clientName) {
where += ` AND client_name = ?`; where += ` AND client_name = $${paramIndex++}`;
params.push(clientName); params.push(clientName);
} }
if (search) { 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}%`); params.push(`%${search}%`, `%${search}%`);
paramIndex += 2;
} }
// Group by patient_name — pick thumbnail from most recent image // 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( const images = await db.all(
`SELECT * FROM images `SELECT * FROM images
WHERE patient_name = ? AND enabled = ? WHERE patient_name = $1 AND enabled = $2
ORDER BY created_at DESC`, ORDER BY created_at DESC`,
[patientName, showDisabled ? 0 : 1] [patientName, showDisabled ? 0 : 1]
); );
@@ -119,7 +123,7 @@ router.get('/by-patient', async (req, res) => {
// ================================================================ // ================================================================
router.get('/:id', async (req, res) => { router.get('/:id', async (req, res) => {
try { 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) { if (!image) {
return res.status(404).json({ error: 'Imagem não encontrada' }); 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) => { router.get('/:id/transformations', async (req, res) => {
try { 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) { if (!image) {
return res.status(404).json({ error: 'Imagem não encontrada' }); return res.status(404).json({ error: 'Imagem não encontrada' });
} }
// Buscar a imagem original se esta é uma transformação // Buscar a imagem original se esta é uma transformação
const originalId = image.original_image_id || image.id; 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); const originalPath = path.join(__dirname, '../uploads', originalImage.filename);
let originalBuffer; let originalBuffer;
@@ -217,7 +221,7 @@ router.get('/:id/transformations', async (req, res) => {
router.post('/:id/transform', async (req, res) => { router.post('/:id/transform', async (req, res) => {
try { try {
const { rotation, flipH, flipV, remark } = req.body; 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) { if (!originalImage) {
return res.status(404).json({ error: 'Imagem não encontrada' }); 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 // Buscar a imagem original real se esta é uma transformação
const realOriginalId = originalImage.original_image_id || originalImage.id; 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 // Processar a imagem
const originalPath = path.join(__dirname, '../uploads', realOriginal.filename); const originalPath = path.join(__dirname, '../uploads', realOriginal.filename);
@@ -250,8 +254,8 @@ router.post('/:id/transform', async (req, res) => {
const processedPath = path.join(__dirname, '../processed', processedFilename); const processedPath = path.join(__dirname, '../processed', processedFilename);
await fs.writeFile(processedPath, processed); await fs.writeFile(processedPath, processed);
// Desabilitar a imagem atual para que não fique duplicada na interface // Desabilitar a imagem active atual para que não fique duplicada na interface
await db.run('UPDATE images SET enabled = 0 WHERE id = ?', [req.params.id]); await db.run('UPDATE images SET enabled = 0 WHERE id = $1', [req.params.id]);
const newRemark = remark !== undefined ? remark : originalImage.remark; const newRemark = remark !== undefined ? remark : originalImage.remark;
@@ -260,7 +264,7 @@ router.post('/:id/transform', async (req, res) => {
`INSERT INTO images ( `INSERT INTO images (
image_guid, patient_name, client_name, original_filename, filename, image_guid, patient_name, client_name, original_filename, filename,
enabled, rotation, flip_horizontal, flip_vertical, original_image_id, created_at, remark, doctor 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.image_guid}_${timestamp}`,
realOriginal.patient_name, realOriginal.patient_name,
@@ -298,11 +302,11 @@ router.put('/:id/patient-info', async (req, res) => {
await db.run( await db.run(
`UPDATE images SET `UPDATE images SET
patient_name_resumed = ?, patient_name_resumed = $1,
tooth_number = ?, tooth_number = $2,
tooth_side = ?, tooth_side = $3,
enabled = 1 enabled = 1
WHERE id = ?`, WHERE id = $4`,
[patientNameResumed, toothNumber, toothSide, req.params.id] [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) => { router.get('/:id/download', async (req, res) => {
try { 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) { if (!image) {
return res.status(404).json({ error: 'Imagem não encontrada' }); 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) => { router.delete('/:id', async (req, res) => {
try { 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) { if (!image) {
return res.status(404).json({ error: 'Imagem não encontrada' }); return res.status(404).json({ error: 'Imagem não encontrada' });
} }
@@ -366,7 +370,7 @@ router.delete('/:id', async (req, res) => {
} }
// Apagar do banco // 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' }); res.json({ success: true, message: 'Imagem apagada com sucesso' });
} catch (error) { } catch (error) {
@@ -380,13 +384,13 @@ router.delete('/:id', async (req, res) => {
// ================================================================ // ================================================================
router.put('/:id/toggle-enabled', async (req, res) => { router.put('/:id/toggle-enabled', async (req, res) => {
try { 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) { if (!image) {
return res.status(404).json({ error: 'Imagem não encontrada' }); return res.status(404).json({ error: 'Imagem não encontrada' });
} }
const newEnabled = image.enabled ? 0 : 1; 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 }); res.json({ success: true, enabled: newEnabled === 1 });
} catch (error) { } 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 // 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) { if (!user) {
return res.status(404).json({ error: 'Usuário não encontrado' }); return res.status(404).json({ error: 'Usuário não encontrado' });
} }
+8 -10
View File
@@ -14,7 +14,6 @@ const jwt = require('jsonwebtoken');
const db = require('./database'); const db = require('./database');
const authRoutes = require('./routes/auth'); const authRoutes = require('./routes/auth');
const imageRoutes = require('./routes/images'); const imageRoutes = require('./routes/images');
const patientsRouter = require('./routes/patients');
const gtosRouter = require('./routes/gtos'); const gtosRouter = require('./routes/gtos');
const systemRoutes = require('./routes/system'); const systemRoutes = require('./routes/system');
const adminClinicsRoutes = require('./routes/admin-clinics'); const adminClinicsRoutes = require('./routes/admin-clinics');
@@ -60,7 +59,6 @@ app.use(cookieParser());
// ================================================================ // ================================================================
// ROTAS DA API // ROTAS DA API
// ================================================================ // ================================================================
app.use('/api/patients', patientsRouter);
// ================================================================ // ================================================================
// ROTAS PÚBLICAS (SEMPRE ACESSÍVEIS) // ROTAS PÚBLICAS (SEMPRE ACESSÍVEIS)
@@ -104,7 +102,7 @@ app.get('/api/clients/check-name', async (req, res) => {
} }
// 1. Verificar se existe no banco de dados na tabela images (busca case-insensitive) // 1. Verificar se existe no banco de dados na tabela images (busca case-insensitive)
const row = await db.get('SELECT COUNT(*) as count FROM images WHERE LOWER(client_name) = LOWER(?)', [name]); const row = await db.get('SELECT COUNT(*) as count FROM images WHERE LOWER(client_name) = LOWER($1)', [name]);
const existsInDb = row && row.count > 0; const existsInDb = row && row.count > 0;
// 2. Verificar se existe algum cliente atualmente conectado com esse nome (desconsiderando o próprio clientId) // 2. Verificar se existe algum cliente atualmente conectado com esse nome (desconsiderando o próprio clientId)
@@ -348,10 +346,10 @@ app.post('/api/install', async (req, res) => {
const passwordHash = await hashPassword(admin.password); const passwordHash = await hashPassword(admin.password);
await db.run( await db.run(
`INSERT INTO users (username, email, password_hash, created_at) `INSERT INTO users (username, email, password_hash, created_at)
VALUES (?, ?, ?, NOW()) VALUES ($1, $2, $3, CURRENT_TIMESTAMP)
ON DUPLICATE KEY UPDATE ON CONFLICT (username) DO UPDATE SET
email = VALUES(email), email = EXCLUDED.email,
password_hash = VALUES(password_hash)`, password_hash = EXCLUDED.password_hash`,
[admin.username, admin.email, passwordHash] [admin.username, admin.email, passwordHash]
); );
console.log('✅ Usuário admin criado:', admin.username); console.log('✅ Usuário admin criado:', admin.username);
@@ -749,7 +747,7 @@ io.on('connection', (socket) => {
// Verificar se a imagem já foi inserida no banco // Verificar se a imagem já foi inserida no banco
const existing = await db.get( const existing = await db.get(
"SELECT id, filename FROM images WHERE original_filename = ? AND enabled = 1 LIMIT 1", "SELECT id, filename FROM images WHERE original_filename = $1 AND enabled = 1 LIMIT 1",
[data.metadata.fileName] [data.metadata.fileName]
); );
if (existing) { if (existing) {
@@ -806,7 +804,7 @@ io.on('connection', (socket) => {
doctor, doctor,
remark, remark,
created_at created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
[ [
data.metadata.guid, data.metadata.guid,
data.patientData.name || 'Paciente não identificado', data.patientData.name || 'Paciente não identificado',
@@ -873,7 +871,7 @@ io.on('connection', (socket) => {
} }
const existing = await db.get( const existing = await db.get(
"SELECT id, filename FROM images WHERE original_filename = ? AND enabled = 1 LIMIT 1", "SELECT id, filename FROM images WHERE original_filename = $1 AND enabled = 1 LIMIT 1",
[data.fileName] [data.fileName]
); );
+3 -3
View File
@@ -79,7 +79,7 @@ async function loadConfigFromDb() {
let hasSettings = false; let hasSettings = false;
for (const k of keys) { for (const k of keys) {
const row = await db.get('SELECT value FROM settings WHERE key = ?', [k]); const row = await db.get('SELECT value FROM settings WHERE key = $1', [k]);
if (row) { if (row) {
// Mapear de snake_case do banco para camelCase do config // Mapear de snake_case do banco para camelCase do config
const camelKey = k.replace(/_([a-z])/g, (g) => g[1].toUpperCase()); const camelKey = k.replace(/_([a-z])/g, (g) => g[1].toUpperCase());
@@ -146,7 +146,7 @@ async function updateConfig(newConfig) {
for (const [key, val] of Object.entries(settingsMap)) { for (const [key, val] of Object.entries(settingsMap)) {
await db.run( await db.run(
`INSERT INTO settings (key, value) `INSERT INTO settings (key, value)
VALUES (?, ?) VALUES ($1, $2)
ON CONFLICT (key) ON CONFLICT (key)
DO UPDATE SET value = EXCLUDED.value`, DO UPDATE SET value = EXCLUDED.value`,
[key, val] [key, val]
@@ -250,7 +250,7 @@ async function getImageBuffer(filename, clientName = null, patientName = null, i
if (!cName || !pName) { if (!cName || !pName) {
try { try {
const db = require('./database'); const db = require('./database');
const row = await db.get('SELECT client_name, patient_name FROM images WHERE filename = ? OR thumb_filename = ? LIMIT 1', [filename, filename]); const row = await db.get('SELECT client_name, patient_name FROM images WHERE filename = $1 OR thumb_filename = $2 LIMIT 1', [filename, filename]);
if (row) { if (row) {
cName = row.client_name; cName = row.client_name;
pName = row.patient_name; pName = row.patient_name;