diff --git a/dental-server-deploy.tar.gz b/dental-server-deploy.tar.gz new file mode 100644 index 0000000..bb9c9e3 Binary files /dev/null and b/dental-server-deploy.tar.gz differ diff --git a/dental-server/auth.js b/dental-server/auth.js index cee2822..d015d0d 100644 --- a/dental-server/auth.js +++ b/dental-server/auth.js @@ -60,14 +60,15 @@ async function verifyPassword(password, hash) { // GERAR TOKEN JWT // ================================================================ -function generateToken(user) { +function generateToken(user, expiresIn = JWT_EXPIRES_IN) { return jwt.sign( { id: user.id, - username: user.username + username: user.username, + is_admin: user.is_admin }, JWT_SECRET, - { expiresIn: JWT_EXPIRES_IN } + { expiresIn: expiresIn } ); } @@ -90,8 +91,8 @@ async function createInitialUser() { // Inserir usuário await db.run( - `INSERT INTO users (username, email, password_hash, created_at) - VALUES (?, ?, ?, NOW())`, + `INSERT INTO users (username, email, password_hash, is_admin, created_at) + VALUES (?, ?, ?, 1, NOW())`, ['rcesar', 'rcesar@rcesar.com', passwordHash] ); diff --git a/dental-server/database.js b/dental-server/database.js index 9a4235c..8da67c8 100644 --- a/dental-server/database.js +++ b/dental-server/database.js @@ -1,4 +1,4 @@ -const { Pool } = require('pg'); +const mysql = require('mysql2/promise'); const fs = require('fs').promises; const path = require('path'); @@ -14,15 +14,18 @@ function rewriteQuery(sql) { // Replace NOW() with datetime('now') query = query.replace(/\bNOW\(\)/gi, "datetime('now')"); + // Rewrite ON DUPLICATE KEY UPDATE for users + if (query.includes('ON DUPLICATE KEY UPDATE')) { + query = `INSERT INTO users (username, email, password_hash, created_at) + VALUES (?, ?, ?, datetime('now')) + ON CONFLICT(username) DO UPDATE SET + email = excluded.email, + password_hash = excluded.password_hash`; + } + return query; } -// Convert MySQL ? to Postgres $1, $2 -function convertSqlForPg(sql) { - let i = 1; - return sql.replace(/\?/g, () => '$' + (i++)); -} - // ================================================================ // INICIALIZAR BANCO DE DADOS // ================================================================ @@ -41,18 +44,13 @@ async function initSQLite() { username TEXT UNIQUE NOT NULL, email TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, + clinic_name TEXT, + pc_name TEXT, + is_admin INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); `); - sqliteDb.exec(` - CREATE TABLE IF NOT EXISTS settings ( - key TEXT PRIMARY KEY, - value TEXT NOT NULL - ); - `); - console.log('✅ Tabela settings SQLite criada/verificada'); - sqliteDb.exec(` CREATE TABLE IF NOT EXISTS images ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -71,7 +69,6 @@ async function initSQLite() { flip_vertical INTEGER DEFAULT 0, doctor TEXT, remark TEXT, - thumb_filename TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); `); @@ -100,16 +97,24 @@ async function initSQLite() { // Migrações para adicionar colunas se o banco já existia try { sqliteDb.exec("ALTER TABLE images ADD COLUMN doctor TEXT"); - } catch (e) {} + } catch (e) { + // Coluna já existe + } + try { sqliteDb.exec("ALTER TABLE images ADD COLUMN remark TEXT"); - } catch (e) {} - try { - sqliteDb.exec("ALTER TABLE images ADD COLUMN thumb_filename TEXT"); - } catch (e) {} + } catch (e) { + // Coluna já existe + } console.log('✅ Tabelas SQLite users e images criadas/verificadas'); + // Migrações para adicionar colunas em users se o banco já existia + try { sqliteDb.exec("ALTER TABLE users ADD COLUMN clinic_name TEXT"); } catch (e) {} + try { sqliteDb.exec("ALTER TABLE users ADD COLUMN pc_name TEXT"); } catch (e) {} + try { sqliteDb.exec("ALTER TABLE users ADD COLUMN is_admin INTEGER DEFAULT 0"); } catch (e) {} + + // Criar administrador padrão se não houver usuários try { const stmtCount = sqliteDb.prepare('SELECT COUNT(*) as count FROM users'); const userCount = stmtCount.get().count; @@ -118,10 +123,10 @@ async function initSQLite() { const bcrypt = require('bcrypt'); const passwordHash = bcrypt.hashSync('admin1234', 10); const stmtInsert = sqliteDb.prepare(` - INSERT INTO users (username, email, password_hash) - VALUES (?, ?, ?) + INSERT INTO users (username, email, password_hash, clinic_name, pc_name, is_admin) + VALUES (?, ?, ?, ?, ?, 1) `); - stmtInsert.run('admin', 'admin@dental.local', passwordHash); + stmtInsert.run('admin', 'admin@dental.local', passwordHash, 'Admin Clinic', 'Admin PC'); console.log('✅ Usuário admin padrão criado com sucesso!'); } } catch (error) { @@ -138,185 +143,250 @@ async function initDatabase() { try { const dbConfig = { host: process.env.DB_HOST || 'localhost', - port: process.env.DB_PORT || 5432, - user: process.env.DB_USER || 'postgres', + port: process.env.DB_PORT || 3306, + user: process.env.DB_USER || 'root', password: process.env.DB_PASSWORD || '', }; const dbName = process.env.DB_NAME || 'dental_images'; - const tempPool = new Pool({ + // Criar pool temporário SEM database para poder criar o banco + const tempPool = mysql.createPool({ ...dbConfig, - database: 'postgres', + waitForConnections: true, + connectionLimit: 10, + queueLimit: 0 }); + console.log('✅ Pool de conexões MySQL criado'); + + // Verificar se banco existe, se não criar try { - const res = await tempPool.query(`SELECT 1 FROM pg_database WHERE datname = $1`, [dbName]); - if (res.rowCount === 0) { + const [databases] = await tempPool.query( + `SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ?`, + [dbName] + ); + + if (databases.length === 0) { console.log(`⚠️ Banco ${dbName} não encontrado. Criando...`); - await tempPool.query(`CREATE DATABASE "${dbName}"`); + await tempPool.query(`CREATE DATABASE ${dbName} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`); console.log(`✅ Banco ${dbName} criado!`); } else { console.log(`✅ Banco ${dbName} já existe`); } } catch (error) { - console.error('❌ Erro ao verificar/criar banco no PostgreSQL:', error.message); - } finally { - await tempPool.end(); + console.error('❌ Erro ao verificar/criar banco:', error); + throw error; } - pool = new Pool({ + // Fechar pool temporário + await tempPool.end(); + + // Criar pool definitivo COM database + pool = mysql.createPool({ ...dbConfig, database: dbName, + waitForConnections: true, + connectionLimit: 10, + queueLimit: 0, + enableKeepAlive: true, + keepAliveInitialDelay: 0 }); - const client = await pool.connect(); - console.log('✅ Conectado ao PostgreSQL'); - client.release(); + // Testar conexão + const connection = await pool.getConnection(); + console.log('✅ Conectado ao MySQL'); + connection.release(); + // Criar tabelas await createTables(); } catch (error) { - console.error('❌ Erro ao conectar ao PostgreSQL:', error.message); + console.error('❌ Erro ao conectar ao MySQL:', error.message); + console.error('Verifique:'); + console.error('1. MySQL está rodando? (sudo systemctl status mysql)'); + console.error('2. Credenciais corretas no .env?'); + console.error('3. Usuário tem permissão para criar databases?'); throw error; } } // ================================================================ -// CRIAR TABELAS POSTGRESQL +// CRIAR TABELAS MYSQL // ================================================================ async function createTables() { - const client = await pool.connect(); + const connection = await pool.getConnection(); try { // Tabela de usuários - await client.query(` + await connection.query(` CREATE TABLE IF NOT EXISTS users ( - id SERIAL PRIMARY KEY, + id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) UNIQUE NOT NULL, email VARCHAR(100) UNIQUE NOT NULL, password_hash TEXT NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) + clinic_name VARCHAR(100) NULL, + pc_name VARCHAR(50) NULL, + is_admin TINYINT(1) DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); - - // Tabela de configurações - await client.query(` - CREATE TABLE IF NOT EXISTS settings ( - key VARCHAR(100) PRIMARY KEY, - value TEXT NOT NULL - ) - `); - console.log('✅ Tabela settings PostgreSQL criada/verificada'); console.log('✅ Tabela users criada/verificada'); // Tabela de imagens - await client.query(` + await connection.query(` CREATE TABLE IF NOT EXISTS images ( - id SERIAL PRIMARY KEY, + id INT AUTO_INCREMENT PRIMARY KEY, image_guid VARCHAR(255) NOT NULL, patient_name TEXT, client_name VARCHAR(100), original_filename TEXT, filename TEXT NOT NULL, - enabled SMALLINT DEFAULT 1, + enabled TINYINT(1) DEFAULT 1, tooth_number VARCHAR(10), tooth_side VARCHAR(10), patient_name_resumed VARCHAR(50), original_image_id INT, rotation INT DEFAULT 0, - flip_horizontal SMALLINT DEFAULT 0, - flip_vertical SMALLINT DEFAULT 0, + flip_horizontal INT DEFAULT 0, + flip_vertical INT DEFAULT 0, doctor VARCHAR(255) NULL, remark TEXT NULL, - thumb_filename VARCHAR(500) NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + INDEX idx_images_enabled (enabled), + INDEX idx_images_original (original_image_id), + INDEX idx_patient_name (patient_name(100)), + INDEX idx_image_guid (image_guid), + INDEX idx_client_name (client_name), + INDEX idx_created_at (created_at) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); - - await client.query(`CREATE INDEX IF NOT EXISTS idx_images_enabled ON images (enabled)`); - await client.query(`CREATE INDEX IF NOT EXISTS idx_images_original ON images (original_image_id)`); - await client.query(`CREATE INDEX IF NOT EXISTS idx_patient_name ON images (patient_name)`); - await client.query(`CREATE INDEX IF NOT EXISTS idx_image_guid ON images (image_guid)`); - await client.query(`CREATE INDEX IF NOT EXISTS idx_client_name ON images (client_name)`); - await client.query(`CREATE INDEX IF NOT EXISTS idx_created_at ON images (created_at)`); - console.log('✅ Tabela images criada/verificada'); // Tabela GTOs - await client.query(` + await connection.query(` CREATE TABLE IF NOT EXISTS gtos ( - id SERIAL PRIMARY KEY, + id INT AUTO_INCREMENT PRIMARY KEY, gto_number VARCHAR(100) NOT NULL, patient_name VARCHAR(255) NOT NULL, description TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + INDEX idx_gtos_patient (patient_name) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); - await client.query(`CREATE INDEX IF NOT EXISTS idx_gtos_patient ON gtos (patient_name)`); console.log('✅ Tabela gtos criada/verificada'); // Tabela de vínculo GTO-Images - await client.query(` + await connection.query(` CREATE TABLE IF NOT EXISTS gto_images ( - id SERIAL PRIMARY KEY, + id INT AUTO_INCREMENT PRIMARY KEY, gto_id INT NOT NULL, image_id INT NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (gto_id) REFERENCES gtos(id) ON DELETE CASCADE, - FOREIGN KEY (image_id) REFERENCES images(id) ON DELETE CASCADE - ) + FOREIGN KEY (image_id) REFERENCES images(id) ON DELETE CASCADE, + UNIQUE INDEX idx_gto_image (gto_id, image_id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); - await client.query(`CREATE UNIQUE INDEX IF NOT EXISTS idx_gto_image ON gto_images (gto_id, image_id)`); console.log('✅ Tabela gto_images criada/verificada'); + // Verificar e adicionar colunas se não existirem (migração) try { - await client.query(`ALTER TABLE images ADD COLUMN client_name VARCHAR(100)`); - await client.query(`CREATE INDEX idx_client_name ON images (client_name)`); - console.log('✅ Coluna client_name adicionada'); - } catch (e) { /* já existe */ } - - try { - await client.query(`ALTER TABLE images ADD COLUMN doctor VARCHAR(255)`); - } catch (e) { /* já existe */ } - - try { - await client.query(`ALTER TABLE images ADD COLUMN remark TEXT`); - } catch (e) { /* já existe */ } - - try { - await client.query(`ALTER TABLE images ADD COLUMN thumb_filename VARCHAR(500)`); - console.log('✅ Coluna thumb_filename adicionada'); - } catch (e) { /* já existe */ } - - // Inserir usuários padrões se a tabela estiver vazia - try { - const userCountRes = await client.query('SELECT COUNT(*) as count FROM users'); - const userCount = parseInt(userCountRes.rows[0].count, 10); - if (userCount === 0) { - console.log('⚠️ Nenhum usuário encontrado no PostgreSQL. Criando usuários padrões...'); - const bcrypt = require('bcrypt'); - const hash1 = await bcrypt.hash('Rc362514', 10); - const hash2 = await bcrypt.hash('admin1234', 10); - - await client.query( - `INSERT INTO users (username, email, password_hash) VALUES ($1, $2, $3)`, - ['rcesar', 'rcesar@rcesar.com', hash1] - ); - await client.query( - `INSERT INTO users (username, email, password_hash) VALUES ($1, $2, $3)`, - ['admin', 'admin@dental.local', hash2] - ); - console.log('✅ Usuários padrões (rcesar e admin) criados com sucesso no PostgreSQL!'); + const columns = await connection.query(` + SELECT COLUMN_NAME + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'images' + AND COLUMN_NAME = 'client_name' + `); + + if (columns[0].length === 0) { + console.log('⚠️ Coluna client_name não encontrada. Adicionando...'); + await connection.query(` + ALTER TABLE images + ADD COLUMN client_name VARCHAR(100) NULL + AFTER patient_name + `); + await connection.query(` + ALTER TABLE images + ADD INDEX idx_client_name (client_name) + `); + console.log('✅ Coluna client_name adicionada'); } - } catch (e) { - console.error('⚠️ Erro ao inicializar usuários padrões no PostgreSQL:', e.message); + } catch (error) { + console.warn('⚠️ Erro ao verificar/adicionar coluna client_name:', error.message); + } + + // Migração para coluna doctor + try { + const columnsDoctor = await connection.query(` + SELECT COLUMN_NAME + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'images' + AND COLUMN_NAME = 'doctor' + `); + if (columnsDoctor[0].length === 0) { + console.log('⚠️ Coluna doctor não encontrada. Adicionando...'); + await connection.query(` + ALTER TABLE images + ADD COLUMN doctor VARCHAR(255) NULL + AFTER client_name + `); + } + } catch (error) { + console.warn('⚠️ Erro ao verificar/adicionar coluna doctor:', error.message); } + // Migração para coluna remark + try { + const columnsRemark = await connection.query(` + SELECT COLUMN_NAME + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'images' + AND COLUMN_NAME = 'remark' + `); + if (columnsRemark[0].length === 0) { + console.log('⚠️ Coluna remark não encontrada. Adicionando...'); + await connection.query(` + ALTER TABLE images + ADD COLUMN remark TEXT NULL + AFTER doctor + `); + } + } catch (error) { + console.warn('⚠️ Erro ao verificar/adicionar coluna remark:', error.message); + } + + // Migração para colunas da tabela users (clinic_name, pc_name, is_admin) + try { + const columnsUsers = await connection.query(` + SELECT COLUMN_NAME + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'users' + AND COLUMN_NAME = 'clinic_name' + `); + if (columnsUsers[0].length === 0) { + console.log('⚠️ Colunas de clínica em users não encontradas. Adicionando...'); + await connection.query(` + ALTER TABLE users + ADD COLUMN clinic_name VARCHAR(100) NULL AFTER password_hash, + ADD COLUMN pc_name VARCHAR(50) NULL AFTER clinic_name, + ADD COLUMN is_admin TINYINT(1) DEFAULT 0 AFTER pc_name + `); + // O primeiro usuário (admin padrão) deve virar admin se já existe + await connection.query(`UPDATE users SET is_admin = 1 ORDER BY id ASC LIMIT 1`); + } + } catch (error) { + console.warn('⚠️ Erro ao verificar/adicionar colunas de clínica em users:', error.message); + } + } catch (error) { console.error('Erro ao criar tabelas:', error); } finally { - client.release(); + connection.release(); } } @@ -339,9 +409,8 @@ function get(query, params = []) { return new Promise(async (resolve, reject) => { try { - const pgQuery = convertSqlForPg(query); - const res = await pool.query(pgQuery, params); - resolve(res.rows[0] || null); + const [rows] = await pool.query(query, params); + resolve(rows[0] || null); } catch (error) { reject(error); } @@ -363,9 +432,8 @@ function all(query, params = []) { return new Promise(async (resolve, reject) => { try { - const pgQuery = convertSqlForPg(query); - const res = await pool.query(pgQuery, params); - resolve(res.rows); + const [rows] = await pool.query(query, params); + resolve(rows); } catch (error) { reject(error); } @@ -390,25 +458,10 @@ function run(query, params = []) { return new Promise(async (resolve, reject) => { try { - let pgQuery = convertSqlForPg(query); - - // Auto-append RETURNING id for INSERT if not present (except for settings table) - if (pgQuery.trim().toUpperCase().startsWith('INSERT') && - !pgQuery.toUpperCase().includes('RETURNING') && - !pgQuery.toUpperCase().includes('SETTINGS')) { - pgQuery += ' RETURNING id'; - } - - const res = await pool.query(pgQuery, params); - - let lastID = null; - if (res.rows && res.rows.length > 0 && res.rows[0].id) { - lastID = res.rows[0].id; - } - + const [result] = await pool.query(query, params); resolve({ - lastID: lastID, - changes: res.rowCount + lastID: result.insertId, + changes: result.affectedRows }); } catch (error) { reject(error); diff --git a/dental-server/package-lock.json b/dental-server/package-lock.json index 8c0bc80..fef42e3 100644 --- a/dental-server/package-lock.json +++ b/dental-server/package-lock.json @@ -16,7 +16,7 @@ "express": "^4.18.2", "express-session": "^1.17.3", "jsonwebtoken": "^9.0.2", - "pg": "^8.11.3", + "mysql2": "^3.6.0", "sharp": "^0.32.6", "socket.io": "^4.7.2" }, @@ -157,6 +157,15 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/b4a": { "version": "1.7.3", "resolved": "https://registry.npmmirror.com/b4a/-/b4a-1.7.3.tgz", @@ -596,6 +605,15 @@ "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", "license": "MIT" }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", @@ -991,6 +1009,15 @@ "node": ">=10" } }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -1226,6 +1253,12 @@ "node": ">=8" } }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, "node_modules/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmmirror.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -1317,6 +1350,36 @@ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "license": "MIT" }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/lru.min": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/lru.min/-/lru.min-1.1.2.tgz", + "integrity": "sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, "node_modules/make-dir": { "version": "3.1.0", "resolved": "https://registry.npmmirror.com/make-dir/-/make-dir-3.1.0.tgz", @@ -1501,6 +1564,54 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/mysql2": { + "version": "3.15.3", + "resolved": "https://registry.npmmirror.com/mysql2/-/mysql2-3.15.3.tgz", + "integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.0", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/mysql2/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "license": "MIT", + "dependencies": { + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/napi-build-utils": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/napi-build-utils/-/napi-build-utils-2.0.0.tgz", @@ -1656,122 +1767,6 @@ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, - "node_modules/pg": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.21.0.tgz", - "integrity": "sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==", - "dependencies": { - "pg-connection-string": "^2.13.0", - "pg-pool": "^3.14.0", - "pg-protocol": "^1.14.0", - "pg-types": "2.2.0", - "pgpass": "1.0.5" - }, - "engines": { - "node": ">= 16.0.0" - }, - "optionalDependencies": { - "pg-cloudflare": "^1.4.0" - }, - "peerDependencies": { - "pg-native": ">=3.0.1" - }, - "peerDependenciesMeta": { - "pg-native": { - "optional": true - } - } - }, - "node_modules/pg-cloudflare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", - "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", - "optional": true - }, - "node_modules/pg-connection-string": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.13.0.tgz", - "integrity": "sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig==" - }, - "node_modules/pg-int8": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", - "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/pg-pool": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", - "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", - "peerDependencies": { - "pg": ">=8.0" - } - }, - "node_modules/pg-protocol": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.14.0.tgz", - "integrity": "sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA==" - }, - "node_modules/pg-types": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", - "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", - "dependencies": { - "pg-int8": "1.0.1", - "postgres-array": "~2.0.0", - "postgres-bytea": "~1.0.0", - "postgres-date": "~1.0.4", - "postgres-interval": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pgpass": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", - "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", - "dependencies": { - "split2": "^4.1.0" - } - }, - "node_modules/postgres-array": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", - "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", - "engines": { - "node": ">=4" - } - }, - "node_modules/postgres-bytea": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", - "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-date": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", - "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-interval": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", - "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", - "dependencies": { - "xtend": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/prebuild-install": { "version": "7.1.3", "resolved": "https://registry.npmmirror.com/prebuild-install/-/prebuild-install-7.1.3.tgz", @@ -2024,6 +2019,11 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmmirror.com/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, "node_modules/serve-static": { "version": "1.16.2", "resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-1.16.2.tgz", @@ -2322,12 +2322,13 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "license": "MIT", "engines": { - "node": ">= 10.x" + "node": ">= 0.6" } }, "node_modules/statuses": { @@ -2588,14 +2589,6 @@ } } }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "engines": { - "node": ">=0.4" - } - }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz", diff --git a/dental-server/package.json b/dental-server/package.json index 921def9..149b0e0 100644 --- a/dental-server/package.json +++ b/dental-server/package.json @@ -19,15 +19,13 @@ "socket.io": "^4.7.2", "express": "^4.18.2", "sharp": "^0.32.6", - "pg": "^8.11.3", + "mysql2": "^3.6.0", "dotenv": "^16.3.1", "cors": "^2.8.5", "bcrypt": "^5.1.1", "jsonwebtoken": "^9.0.2", "cookie-parser": "^1.4.6", - "express-session": "^1.17.3", - "@aws-sdk/client-s3": "^3.500.0", - "@aws-sdk/lib-storage": "^3.500.0" + "express-session": "^1.17.3" }, "engines": { "node": ">=16.0.0", diff --git a/dental-server/public/app.js b/dental-server/public/app.js index f706ea7..ef08e9c 100644 --- a/dental-server/public/app.js +++ b/dental-server/public/app.js @@ -39,6 +39,7 @@ function getAuthToken() { } let authVerificationInProgress = false; +let currentUser = null; async function verifyAuth() { if (authVerificationInProgress) return false; @@ -52,7 +53,14 @@ async function verifyAuth() { if (!response.ok) { localStorage.removeItem('auth_token'); window.location.href = '/login'; return false; } const data = await response.json(); authVerificationInProgress = false; - if (data.valid) return true; + if (data.valid) { + currentUser = data.user; + if (currentUser && currentUser.is_admin) { + const navAdmin = document.getElementById('navAdminClinics'); + if (navAdmin) navAdmin.style.display = 'flex'; + } + return true; + } localStorage.removeItem('auth_token'); window.location.href = '/login'; return false; } catch { localStorage.removeItem('auth_token'); @@ -98,14 +106,13 @@ let clientsList = []; let selectedClient = ''; let showDisabled = false; let currentTransformations = []; -let fineTuneAngle = 0; // ================================================================ // DOM REFS // ================================================================ const searchInput = document.getElementById('searchInput'); -const clientFilter = document.getElementById('clientFilter'); // Pode ser null se removido do header +const clientFilter = document.getElementById('clientFilter'); const toggleDisabledBtn = document.getElementById('toggleDisabledBtn'); const toggleText = document.getElementById('toggleText'); const imagesGrid = document.getElementById('imagesGrid'); @@ -150,7 +157,6 @@ document.addEventListener('DOMContentLoaded', async () => { await loadClientsList(); loadPatients(); setupEventListeners(); - initDragAndDrop(); setInterval(loadClientsList, 5000); } catch (error) { _log.error('INIT', 'Erro fatal na inicialização:', error.message, error); @@ -170,12 +176,10 @@ function setupEventListeners() { else loadPatientImages(selectedPatient.patient_name); }, 400)); - if (clientFilter) { - clientFilter.addEventListener('change', e => { - selectedClient = e.target.value; - if (view === 'patients') loadPatients(); - }); - } + clientFilter.addEventListener('change', e => { + selectedClient = e.target.value; + if (view === 'patients') loadPatients(); + }); toggleDisabledBtn.addEventListener('click', () => { showDisabled = !showDisabled; @@ -188,14 +192,10 @@ function setupEventListeners() { // Transform modal close on backdrop transformModal.addEventListener('click', e => { - if (e.target === transformModal) { - transformModal.classList.remove('active'); - if (typeof clearTransformSelection === 'function') clearTransformSelection(); - } + if (e.target === transformModal) transformModal.classList.remove('active'); }); document.getElementById('closeTransformModal').addEventListener('click', () => { transformModal.classList.remove('active'); - if (typeof clearTransformSelection === 'function') clearTransformSelection(); }); } @@ -227,85 +227,22 @@ async function loadClientsList() { } function updateClientsDropdown() { - // Se por acaso o elemento clientFilter ainda existir no DOM em alguma outra tela, atualiza ele - if (clientFilter) { - const current = clientFilter.value; - clientFilter.innerHTML = ''; - const sorted = [...clientsList].sort((a, b) => { - const o = { identified: 1, connected: 2, historic: 3 }; - return (o[a.status] || 99) - (o[b.status] || 99); - }); - sorted.forEach(c => { - const opt = document.createElement('option'); - opt.value = c.name; - let lbl = c.name; - if (c.name === 'Upload Web') { - lbl = 'Upload Web 💻 (Imagens do Painel)'; - } else { - if (c.status === 'identified') lbl += ' ✅'; - else if (c.status === 'historic') lbl += ' 📚'; - } - opt.textContent = lbl; - clientFilter.appendChild(opt); - }); - clientFilter.value = current || selectedClient; - } - - // Renderizar o submenu dinâmico na sidebar - const submenu = document.getElementById('clientsSubmenu'); - if (!submenu) return; - + const current = clientFilter.value; + clientFilter.innerHTML = ''; const sorted = [...clientsList].sort((a, b) => { const o = { identified: 1, connected: 2, historic: 3 }; return (o[a.status] || 99) - (o[b.status] || 99); }); - - submenu.innerHTML = ''; - - // Adiciona o item "Todos os Clientes" - const allItem = document.createElement('div'); - allItem.className = `nav-item${selectedClient === '' ? ' active' : ''}`; - allItem.innerHTML = `📋 `; - allItem.onclick = () => selectClientFilter(''); - submenu.appendChild(allItem); - sorted.forEach(c => { - const item = document.createElement('div'); - item.className = `nav-item${selectedClient === c.name ? ' active' : ''}`; + const opt = document.createElement('option'); + opt.value = c.name; let lbl = c.name; - if (c.name === 'Upload Web') { - lbl = 'Upload Web 💻'; - } else { - if (c.status === 'identified') lbl += ' ✅'; - else if (c.status === 'historic') lbl += ' 📚'; - } - item.innerHTML = `🔹 `; - item.onclick = () => selectClientFilter(c.name); - submenu.appendChild(item); + if (c.status === 'identified') lbl += ' ✅'; + else if (c.status === 'historic') lbl += ' 📚'; + opt.textContent = lbl; + clientFilter.appendChild(opt); }); -} - -window.toggleClientsSubmenu = function() { - const submenu = document.getElementById('clientsSubmenu'); - const chevron = document.getElementById('clientsChevron'); - if (!submenu) return; - if (submenu.style.display === 'none') { - submenu.style.display = 'flex'; - if (chevron) chevron.style.transform = 'rotate(180deg)'; - } else { - submenu.style.display = 'none'; - if (chevron) chevron.style.transform = 'rotate(0deg)'; - } -} - -function selectClientFilter(clientName) { - selectedClient = clientName; - if (view === 'patients') { - loadPatients(); - } else { - showPatientsView(); - } - updateClientsDropdown(); + clientFilter.value = current || selectedClient; } // ================================================================ @@ -317,11 +254,6 @@ function showPatientsView() { selectedPatient = null; backBtn.style.display = 'none'; patientHeader.style.display = 'none'; - - // Restaurar título do header - const mainTitle = document.getElementById('mainTitle'); - if (mainTitle) mainTitle.textContent = 'Galeria de Imagens'; - loadPatients(); } @@ -417,7 +349,6 @@ function renderPatientCards() { return `
Crie o acesso para obter o token de conexão do notebook
+Guarde e insira os dados abaixo no instalador Windows
+