feat: adicionar gerenciamento de clínicas e tokens
This commit is contained in:
Binary file not shown.
@@ -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]
|
||||
);
|
||||
|
||||
|
||||
+197
-144
@@ -1,4 +1,4 @@
|
||||
const { Pool } = require('pg');
|
||||
const mysql = require('mysql2/promise');
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
|
||||
@@ -14,13 +14,16 @@ function rewriteQuery(sql) {
|
||||
// Replace NOW() with datetime('now')
|
||||
query = query.replace(/\bNOW\(\)/gi, "datetime('now')");
|
||||
|
||||
return query;
|
||||
}
|
||||
// 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`;
|
||||
}
|
||||
|
||||
// Convert MySQL ? to Postgres $1, $2
|
||||
function convertSqlForPg(sql) {
|
||||
let i = 1;
|
||||
return sql.replace(/\?/g, () => '$' + (i++));
|
||||
return query;
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
@@ -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 */ }
|
||||
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'
|
||||
`);
|
||||
|
||||
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!');
|
||||
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);
|
||||
|
||||
Generated
+123
-130
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
+161
-819
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,468 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ScoreOdonto - Cadastro de Clínica</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0b0a12;
|
||||
--card: rgba(18, 16, 28, 0.85);
|
||||
--border: rgba(255, 255, 255, 0.08);
|
||||
--primary: #7c4dff;
|
||||
--primary-hover: #651fff;
|
||||
--accent: #00e5ff;
|
||||
--success: #00e676;
|
||||
--danger: #ff1744;
|
||||
--text: #f5f5f7;
|
||||
--muted: #8e8e93;
|
||||
--mono: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bg);
|
||||
background-image:
|
||||
radial-gradient(at 0% 0%, rgba(124, 77, 255, 0.2) 0px, transparent 60%),
|
||||
radial-gradient(at 100% 100%, rgba(0, 229, 255, 0.12) 0px, transparent 60%);
|
||||
background-attachment: fixed;
|
||||
color: var(--text);
|
||||
font-family: 'Inter', sans-serif;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 24px;
|
||||
padding: 40px 30px;
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
box-shadow: 0 20px 50px rgba(0,0,0,0.5);
|
||||
transition: all 0.4s ease;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 35px;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 10px;
|
||||
display: inline-block;
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-8px); }
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #fff 0%, var(--muted) 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
font-size: 0.9rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 0.85rem;
|
||||
color: var(--muted);
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
background: rgba(0,0,0,0.4);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 14px 16px;
|
||||
color: #fff;
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 0.95rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px rgba(0, 229, 255, 0.15);
|
||||
background: rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-hover) 100%);
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 8px 20px rgba(124, 77, 255, 0.3);
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 12px 24px rgba(124, 77, 255, 0.4);
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
background: var(--muted);
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* ---- SUCCESS SCREEN ---- */
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.success-header {
|
||||
text-align: center;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.success-badge {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background: rgba(0, 230, 118, 0.1);
|
||||
border: 1px solid var(--success);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.8rem;
|
||||
margin: 0 auto 15px auto;
|
||||
color: var(--success);
|
||||
box-shadow: 0 0 20px rgba(0, 230, 118, 0.2);
|
||||
}
|
||||
|
||||
.success-header h2 {
|
||||
font-size: 1.5rem;
|
||||
color: #fff;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.success-instructions {
|
||||
background: rgba(124, 77, 255, 0.05);
|
||||
border: 1px dashed rgba(124, 77, 255, 0.2);
|
||||
padding: 15px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text);
|
||||
margin-bottom: 25px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.copy-card {
|
||||
background: rgba(0,0,0,0.5);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
padding: 18px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.copy-item {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.copy-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.copy-item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.copy-label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--accent);
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.copy-btn:hover {
|
||||
background: rgba(0, 229, 255, 0.1);
|
||||
}
|
||||
|
||||
.copy-val {
|
||||
font-family: var(--mono);
|
||||
font-size: 0.9rem;
|
||||
color: #fff;
|
||||
background: rgba(255,255,255,0.03);
|
||||
border: 1px solid rgba(255,255,255,0.05);
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
word-break: break-all;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.btn-copy-all {
|
||||
background: linear-gradient(135deg, #00e5ff 0%, #00b0ff 100%);
|
||||
color: #0b0a12;
|
||||
box-shadow: 0 8px 20px rgba(0, 229, 255, 0.25);
|
||||
}
|
||||
|
||||
.btn-copy-all:hover {
|
||||
box-shadow: 0 12px 24px rgba(0, 229, 255, 0.35);
|
||||
}
|
||||
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 30px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(100px);
|
||||
background: #fff;
|
||||
color: #0b0a12;
|
||||
padding: 12px 24px;
|
||||
border-radius: 50px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
||||
transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.toast.show {
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background: rgba(255, 23, 68, 0.1);
|
||||
border: 1px solid var(--danger);
|
||||
color: #ff5252;
|
||||
padding: 12px 16px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 20px;
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="toast" class="toast">Texto copiado!</div>
|
||||
|
||||
<div class="container">
|
||||
<!-- 1. REGISTRATION FORM PANEL -->
|
||||
<div id="panel-register">
|
||||
<header class="header">
|
||||
<span class="logo-icon">🦷</span>
|
||||
<h1>Cadastrar Clínica</h1>
|
||||
<p>Crie o acesso para obter o token de conexão do notebook</p>
|
||||
</header>
|
||||
|
||||
<div id="error-alert" class="alert-error">Erro ao cadastrar. Tente novamente.</div>
|
||||
|
||||
<form id="register-form">
|
||||
<div class="form-group">
|
||||
<label for="clinicName">🏥 Nome da Clínica</label>
|
||||
<input type="text" id="clinicName" placeholder="Ex: Clínica Score Dente" required autocomplete="off">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email">📧 E-mail de Conexão</label>
|
||||
<input type="email" id="email" placeholder="Ex: conexao@clinica.com" required autocomplete="off">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="pcName">🖥️ Nome do Computador (PC)</label>
|
||||
<input type="text" id="pcName" placeholder="Ex: NOTEBOOK-CONSULTORIO" required autocomplete="off">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">🔑 Senha de Conexão</label>
|
||||
<input type="password" id="password" placeholder="Crie uma senha de acesso" required>
|
||||
</div>
|
||||
<button type="submit" id="btn-submit" class="btn">🚀 Cadastrar e Gerar Token</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 2. SUCCESS DATA PANEL -->
|
||||
<div id="panel-success" class="hidden">
|
||||
<div class="success-header">
|
||||
<div class="success-badge">✓</div>
|
||||
<h2>Clínica Registrada!</h2>
|
||||
<p>Guarde e insira os dados abaixo no instalador Windows</p>
|
||||
</div>
|
||||
|
||||
<div class="success-instructions">
|
||||
Abra a tela de <strong>Configurações do Dental Client</strong> no computador local, e cole os campos abaixo exatamente como exibidos.
|
||||
</div>
|
||||
|
||||
<div class="copy-card">
|
||||
<div class="copy-item">
|
||||
<div class="copy-item-header">
|
||||
<span class="copy-label">E-mail do Servidor</span>
|
||||
<button class="copy-btn" onclick="copyText('val-email')">Copiar</button>
|
||||
</div>
|
||||
<span id="val-email" class="copy-val">teste@clinica.com</span>
|
||||
</div>
|
||||
|
||||
<div class="copy-item">
|
||||
<div class="copy-item-header">
|
||||
<span class="copy-label">Senha do Servidor</span>
|
||||
<button class="copy-btn" onclick="copyText('val-password')">Copiar</button>
|
||||
</div>
|
||||
<span id="val-password" class="copy-val">••••••••</span>
|
||||
</div>
|
||||
|
||||
<div class="copy-item">
|
||||
<div class="copy-item-header">
|
||||
<span class="copy-label">Token do Servidor</span>
|
||||
<button class="copy-btn" onclick="copyText('val-token')">Copiar</button>
|
||||
</div>
|
||||
<span id="val-token" class="copy-val">token_gerado_no_servidor</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-copy-all" onclick="copyAllData()">📋 Copiar Todos os Dados</button>
|
||||
<button class="btn btn-outline" style="background:transparent; border: 1px solid var(--border); margin-top:10px;" onclick="resetForm()">Cadastrar Outra Clínica</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const registerForm = document.getElementById('register-form');
|
||||
const btnSubmit = document.getElementById('btn-submit');
|
||||
const errorAlert = document.getElementById('error-alert');
|
||||
const panelRegister = document.getElementById('panel-register');
|
||||
const panelSuccess = document.getElementById('panel-success');
|
||||
|
||||
// Form fields values stored temporarily for display/copy
|
||||
let registeredEmail = '';
|
||||
let registeredPassword = '';
|
||||
let registeredToken = '';
|
||||
|
||||
function showToast(message) {
|
||||
const toast = document.getElementById('toast');
|
||||
toast.textContent = message;
|
||||
toast.classList.add('show');
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('show');
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
registerForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const clinicName = document.getElementById('clinicName').value.trim();
|
||||
const email = document.getElementById('email').value.trim();
|
||||
const pcName = document.getElementById('pcName').value.trim();
|
||||
const password = document.getElementById('password').value;
|
||||
|
||||
btnSubmit.disabled = true;
|
||||
btnSubmit.textContent = 'Processando...';
|
||||
errorAlert.style.display = 'none';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/auth/register-clinic', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ clinicName, email, pcName, password })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.success) {
|
||||
// Save for displays
|
||||
registeredEmail = data.clinic.email;
|
||||
registeredPassword = password; // Raw password from form
|
||||
registeredToken = data.clinic.token;
|
||||
|
||||
// Populate success display values
|
||||
document.getElementById('val-email').textContent = registeredEmail;
|
||||
document.getElementById('val-password').textContent = registeredPassword;
|
||||
document.getElementById('val-token').textContent = registeredToken;
|
||||
|
||||
// Transition panels
|
||||
panelRegister.classList.add('hidden');
|
||||
panelSuccess.classList.remove('hidden');
|
||||
showToast('Cadastro concluído com sucesso!');
|
||||
} else {
|
||||
errorAlert.textContent = data.error || 'Erro ao realizar o cadastro';
|
||||
errorAlert.style.display = 'block';
|
||||
}
|
||||
} catch (err) {
|
||||
errorAlert.textContent = 'Erro de conexão com o servidor: ' + err.message;
|
||||
errorAlert.style.display = 'block';
|
||||
} finally {
|
||||
btnSubmit.disabled = false;
|
||||
btnSubmit.textContent = '🚀 Cadastrar e Gerar Token';
|
||||
}
|
||||
});
|
||||
|
||||
function copyText(elementId) {
|
||||
const val = document.getElementById(elementId).textContent;
|
||||
navigator.clipboard.writeText(val)
|
||||
.then(() => showToast('Copiado para a área de transferência!'))
|
||||
.catch(() => showToast('Erro ao copiar'));
|
||||
}
|
||||
|
||||
function copyAllData() {
|
||||
const allText = `ScoreOdonto Dental Client - Credenciais de Conexão\\n\\n` +
|
||||
`E-mail do Servidor: \${registeredEmail}\\n` +
|
||||
`Senha do Servidor : \${registeredPassword}\\n` +
|
||||
`Token do Servidor : \${registeredToken}\\n`;
|
||||
|
||||
navigator.clipboard.writeText(allText)
|
||||
.then(() => showToast('Todos os dados copiados com sucesso!'))
|
||||
.catch(() => showToast('Erro ao copiar'));
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
registerForm.reset();
|
||||
panelSuccess.classList.add('hidden');
|
||||
panelRegister.classList.remove('hidden');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+65
-179
@@ -6,7 +6,7 @@
|
||||
<title>Gerenciador de Imagens Dentais</title>
|
||||
<meta name="description" content="Sistema de gerenciamento de imagens de raio-X dental">
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||||
<link rel="stylesheet" href="/style.css?v=2.7">
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-container">
|
||||
@@ -19,33 +19,18 @@
|
||||
<a href="/" class="nav-item active">
|
||||
<span class="icon">🖼️</span> Pacientes
|
||||
</a>
|
||||
|
||||
<!-- Menu Accordion para Sistemas Clientes -->
|
||||
<div class="nav-item" id="clientsAccordionToggle" onclick="toggleClientsSubmenu()" style="cursor:pointer; display: flex; align-items: center; justify-content: space-between;">
|
||||
<div style="display: flex; align-items: center; gap: 10px; overflow: hidden;">
|
||||
<span class="icon">🏢</span> <span style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">Sistemas Clientes</span>
|
||||
</div>
|
||||
<span id="clientsChevron" style="font-size: 0.8rem; transition: transform 0.3s;">▼</span>
|
||||
</div>
|
||||
|
||||
<!-- Submenu dos Clientes -->
|
||||
<div id="clientsSubmenu" class="sidebar-submenu" style="display: none; padding-left: 10px; background: rgba(0,0,0,0.02);"></div>
|
||||
|
||||
<a href="#" class="nav-item" onclick="openCreatePatientModal(); return false;">
|
||||
<span class="icon">➕</span> Novo Paciente
|
||||
</a>
|
||||
<a href="/clients" class="nav-item">
|
||||
<span class="icon">🖥️</span> Dados do Cliente
|
||||
<span class="icon">🖥️</span> Dispositivos
|
||||
</a>
|
||||
<a href="#" class="nav-item admin-only" id="navAdminClinics" style="display: none;" onclick="showAdminClinics(); return false;">
|
||||
<span class="icon">🏢</span> Gerenciar Clínicas
|
||||
</a>
|
||||
<a href="#" class="nav-item" onclick="openSettingsModal(); return false;">
|
||||
<span class="icon">⚙️</span> Configurações
|
||||
</a>
|
||||
<a href="#" class="nav-item" onclick="openPluginsModal(); return false;">
|
||||
<span class="icon">🔌</span> Plugins / Wasabi
|
||||
</a>
|
||||
<a href="#" class="nav-item" onclick="openSyncModal(); return false;">
|
||||
<span class="icon">🔄</span> Sincronização
|
||||
</a>
|
||||
<a href="/reset" class="nav-item" style="color: #dc3545;">
|
||||
<span class="icon">⚠️</span> Zerar Sistema
|
||||
</a>
|
||||
@@ -68,6 +53,9 @@
|
||||
<h1 id="mainTitle">Galeria de Imagens</h1>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<select id="clientFilter" class="client-filter">
|
||||
<option value="">📋 Todos os Clientes</option>
|
||||
</select>
|
||||
<input
|
||||
type="search"
|
||||
id="searchInput"
|
||||
@@ -105,6 +93,30 @@
|
||||
<p>As imagens aparecerão aqui quando forem enviadas pelo cliente Windows</p>
|
||||
</div>
|
||||
|
||||
<!-- Admin Clínicas State -->
|
||||
<div id="adminClinicsState" style="display: none; padding: 20px;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||||
<h2>🏢 Gerenciamento de Clínicas</h2>
|
||||
<button class="btn btn-primary" onclick="openCreateClinicModal()">+ Nova Clínica</button>
|
||||
</div>
|
||||
<div class="table-responsive" style="background: white; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.05); overflow: hidden;">
|
||||
<table class="data-table" id="clinicsTable" style="width: 100%; border-collapse: collapse; text-align: left;">
|
||||
<thead style="background: #f8f9fa; border-bottom: 1px solid #eee;">
|
||||
<tr>
|
||||
<th style="padding: 15px;">ID</th>
|
||||
<th style="padding: 15px;">Login (Username)</th>
|
||||
<th style="padding: 15px;">Clínica</th>
|
||||
<th style="padding: 15px;">Email</th>
|
||||
<th style="padding: 15px;">Nome PC</th>
|
||||
<th style="padding: 15px; text-align: center;">Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="clinicsTableBody">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Images / Patients Grid -->
|
||||
<div id="imagesGrid" class="images-grid"> </div>
|
||||
|
||||
@@ -137,30 +149,14 @@
|
||||
<!-- View 1: Transforms -->
|
||||
<div id="transformViewWrapper" style="display: flex; flex-direction: column; flex: 1; min-height: 0; height: 100%;">
|
||||
<div id="transformOptions" class="transform-grid" style="flex: 1; min-height: 0;"></div>
|
||||
<div id="transformActionArea" style="display: none; margin-top: 15px; background: #f8f9fa; padding: 15px; border-radius: 8px; flex-shrink: 0; border: 1px solid #e3e8ee;">
|
||||
<textarea id="newImageRemark" class="search-input" rows="2" style="width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 6px;" placeholder="Nova observação (opcional)"></textarea>
|
||||
<div id="transformActionArea" style="display: none; margin-top: 20px; background: #f8f9fa; padding: 15px; border-radius: 8px; flex-shrink: 0;">
|
||||
<label style="font-weight: 600; display: block; margin-bottom: 8px;">Nova Observação / Detalhes (Opcional):</label>
|
||||
<textarea id="newImageRemark" class="search-input" rows="2" style="width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 6px;" placeholder="Ex: Raio-X Dente 45..."></textarea>
|
||||
<div style="display: flex; gap: 10px; margin-top: 10px;">
|
||||
<button class="btn btn-secondary" style="flex: 1; padding: 12px; font-size: 1.1rem;" onclick="clearTransformSelection()">Cancelar</button>
|
||||
<button id="btnSaveTransform" class="btn btn-primary" style="flex: 2; padding: 12px; font-size: 1.1rem;" onclick="saveSelectedTransform()">Salvar Nova Imagem</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bloco de Ajuste Fino (Integrado na legenda do card selecionado) -->
|
||||
<div id="fineTuneContainer" style="display: none;" onclick="event.stopPropagation()">
|
||||
<div class="fine-tune-left-btns">
|
||||
<button type="button" onclick="adjustFineTune(-5)" title="Rotacionar -5°">-5°</button>
|
||||
<button type="button" onclick="adjustFineTune(-1)" title="Rotacionar -1°">-1°</button>
|
||||
</div>
|
||||
|
||||
<div id="fineTuneLabelPlaceholder"></div>
|
||||
|
||||
<div class="fine-tune-right-btns">
|
||||
<button type="button" onclick="adjustFineTune(1)" title="Rotacionar +1°">+1°</button>
|
||||
<button type="button" onclick="adjustFineTune(5)" title="Rotacionar +5°">+5°</button>
|
||||
</div>
|
||||
|
||||
<button type="button" id="btnResetFineTune" onclick="resetFineTune()" style="display: none;" title="Resetar ajuste fino">↩️ Reset</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- View 2: GTO List & Link -->
|
||||
@@ -301,171 +297,61 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Plugins / Wasabi Modal -->
|
||||
<div id="pluginsModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<!-- Modal Nova Clínica -->
|
||||
<div class="modal" id="createClinicModal">
|
||||
<div class="modal-content" style="max-width: 500px; height: auto; border-radius: var(--radius);">
|
||||
<div class="modal-header">
|
||||
<h2>🔌 Configuração do Wasabi Storage</h2>
|
||||
<span class="modal-close" onclick="closePluginsModal()">×</span>
|
||||
<h2>Nova Clínica (Acesso Desktop)</h2>
|
||||
<div class="modal-close" onclick="closeCreateClinicModal()">×</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="pluginsForm" onsubmit="updatePluginsConfig(event)">
|
||||
<div style="background: #f8f9fa; padding: 12px 18px; border-radius: 8px; border: 1px solid #eee; margin-bottom: 20px; font-size: 0.95rem; color: #555;">
|
||||
Configure as credenciais e bucket do **Wasabi S3** para habilitar o armazenamento na nuvem. Se desativado, o sistema usará o armazenamento local do servidor de forma automática.
|
||||
</div>
|
||||
|
||||
<form id="createClinicForm" onsubmit="submitCreateClinic(event)">
|
||||
<div class="form-group">
|
||||
<label for="wasabiAccessKey">Access Key *</label>
|
||||
<input type="text" id="wasabiAccessKey" placeholder="Ex: AIPL5M4G..." required>
|
||||
<label>Login (Username) *</label>
|
||||
<input type="text" id="clinicUsername" class="form-control" required placeholder="Ex: clinica_sorriso">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="wasabiSecretKey">Secret Key *</label>
|
||||
<input type="password" id="wasabiSecretKey" placeholder="Sua Secret Key" required>
|
||||
<label>Email *</label>
|
||||
<input type="email" id="clinicEmail" class="form-control" required placeholder="Ex: contato@clinicasorriso.com">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="wasabiBucket">Bucket Ativo *</label>
|
||||
<input type="text" id="wasabiBucket" placeholder="Ex: meu-bucket-rx" required>
|
||||
<label>Senha de Acesso *</label>
|
||||
<input type="password" id="clinicPassword" class="form-control" required placeholder="Senha para o cliente desktop">
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 12px;">
|
||||
<div class="form-group" style="flex: 1;">
|
||||
<label for="wasabiRegion">Região</label>
|
||||
<input type="text" id="wasabiRegion" placeholder="Ex: us-east-1 (padrão)">
|
||||
</div>
|
||||
<div class="form-group" style="flex: 1;">
|
||||
<label for="wasabiEndpoint">Endpoint</label>
|
||||
<input type="text" id="wasabiEndpoint" placeholder="Ex: s3.wasabisys.com">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Nome da Clínica (Opcional)</label>
|
||||
<input type="text" id="clinicName" class="form-control" placeholder="Ex: Clínica Odonto Sorriso">
|
||||
</div>
|
||||
|
||||
<div id="pluginsStatusAlert" style="display: none; padding: 10px; border-radius: 6px; font-size: 0.9rem; margin-top: 15px;"></div>
|
||||
|
||||
<div class="form-actions" style="margin-top: 25px; display: flex; justify-content: flex-end; gap: 10px; border-top: 1px solid #eee; padding-top: 15px;">
|
||||
<button type="button" class="btn btn-secondary" onclick="closePluginsModal()">Cancelar</button>
|
||||
<button type="submit" class="btn btn-primary" id="btnSavePlugins">Salvar Configurações</button>
|
||||
<div class="form-group">
|
||||
<label>Nome do PC (Opcional)</label>
|
||||
<input type="text" id="clinicPcName" class="form-control" placeholder="Ex: PC-Recepção">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" style="width: 100%; margin-top: 15px;">Cadastrar Clínica</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sync / Upload Modal -->
|
||||
<div id="syncModal" class="modal">
|
||||
<div class="modal-content" style="max-width: 650px; height: auto; border-radius: var(--radius);">
|
||||
<!-- Modal Mostrar Token -->
|
||||
<div class="modal" id="showTokenModal">
|
||||
<div class="modal-content" style="max-width: 600px; border-radius: var(--radius);">
|
||||
<div class="modal-header">
|
||||
<h2>🔄 Área de Sincronização</h2>
|
||||
<span class="modal-close" onclick="closeSyncModal()">×</span>
|
||||
<h2>🔑 Token de Acesso Desktop</h2>
|
||||
<div class="modal-close" onclick="closeShowTokenModal()">×</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="sync-tabs">
|
||||
<button type="button" id="tabBtnDevices" class="sync-tab-btn active" onclick="switchSyncTab('devices')">💻 Dispositivos Conectados</button>
|
||||
<button type="button" id="tabBtnUpload" class="sync-tab-btn" onclick="switchSyncTab('upload')">📤 Envio Manual</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab 1: Dispositivos -->
|
||||
<div id="tab-devices" class="sync-tab-content active">
|
||||
<div style="background: #f8f9fa; padding: 15px; border-radius: 8px; border: 1px solid #eee; margin-bottom: 20px; font-size: 0.95rem; color: #555; line-height: 1.5;">
|
||||
Os computadores clientes com Windows instalados e ativos enviam imagens em tempo real. Você pode solicitar que eles façam uma sincronização manual forçada com o servidor enviando um sinal.
|
||||
</div>
|
||||
|
||||
<div id="syncDevicesLoading" class="loading" style="padding: 20px 0;">
|
||||
<div class="spinner" style="width: 30px; height: 30px; margin-bottom: 10px;"></div>
|
||||
<p>Buscando dispositivos online...</p>
|
||||
</div>
|
||||
|
||||
<div id="syncDevicesEmpty" style="display: none; text-align: center; padding: 30px 20px; color: #666;">
|
||||
<span style="font-size: 2.5rem; display: block; margin-bottom: 10px;">📭</span>
|
||||
<p>Nenhum dispositivo Windows conectado no momento.</p>
|
||||
</div>
|
||||
|
||||
<div id="syncDeviceList" class="sync-device-list" style="display: none;">
|
||||
<!-- Preenchido via Javascript -->
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 25px; border-top: 1px solid #eee; padding-top: 15px; display: flex; justify-content: flex-end; gap: 10px;">
|
||||
<button type="button" class="btn btn-secondary" onclick="closeSyncModal()">Fechar</button>
|
||||
<button type="button" class="btn btn-primary" onclick="triggerSyncFromModal()" id="btnTriggerSyncModal">
|
||||
<span class="icon">🔄</span> Enviar Sinal de Sincronização
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab 2: Envio Manual -->
|
||||
<div id="tab-upload" class="sync-tab-content">
|
||||
<form id="manualUploadForm" onsubmit="handleManualUploadSubmit(event)">
|
||||
<div class="form-group">
|
||||
<label for="uploadPatientSelect">Paciente *</label>
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<select id="uploadPatientSelect" onchange="toggleNewPatientFields()" required style="flex: 1;">
|
||||
<option value="">-- Selecione o Paciente --</option>
|
||||
<option value="NEW_PATIENT">➕ Novo Paciente...</option>
|
||||
</select>
|
||||
<button type="button" class="btn btn-secondary" onclick="refreshUploadPatients()" title="Atualizar Lista" style="width: 44px; padding: 0; display: flex; align-items: center; justify-content: center;">
|
||||
🔄
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Campos para Novo Paciente (ocultos por padrão) -->
|
||||
<div id="newPatientFields" style="display: none; background: #f8f9fa; padding: 15px; border-radius: 8px; border: 1px solid #eee; margin-bottom: 18px; animation: slideDown 0.2s ease;">
|
||||
<h4 style="margin: 0 0 12px 0; color: var(--dark-color); font-size: 0.95rem;">Dados do Novo Paciente</h4>
|
||||
<div style="display: flex; gap: 12px; margin-bottom: 12px;">
|
||||
<div style="flex: 1;">
|
||||
<label style="font-size: 0.82rem; font-weight: 600; display: block; margin-bottom: 5px;">Nome *</label>
|
||||
<input type="text" id="uploadNewPatientFirstName" placeholder="Ex: Maria" style="width: 100%; padding: 8px 12px; border: 1px solid #ccc; border-radius: 4px;">
|
||||
</div>
|
||||
<div style="flex: 1;">
|
||||
<label style="font-size: 0.82rem; font-weight: 600; display: block; margin-bottom: 5px;">Sobrenome *</label>
|
||||
<input type="text" id="uploadNewPatientLastName" placeholder="Ex: Oliveira" style="width: 100%; padding: 8px 12px; border: 1px solid #ccc; border-radius: 4px;">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-size: 0.82rem; font-weight: 600; display: block; margin-bottom: 5px;">Dentista Responsável</label>
|
||||
<input type="text" id="uploadNewPatientDoctor" placeholder="Nome do dentista (opcional)" style="width: 100%; padding: 8px 12px; border: 1px solid #ccc; border-radius: 4px;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="uploadRemark">Observações / Detalhes (Opcional)</label>
|
||||
<textarea id="uploadRemark" placeholder="Ex: Raio-X Panorâmico" rows="2" style="width: 100%; padding: 10px; border: 2px solid var(--border-color); border-radius: var(--radius-sm); font-family: inherit; font-size: 1rem; resize: vertical;"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Zona de Drag and Drop -->
|
||||
<div class="drag-drop-zone" id="uploadDragDropZone">
|
||||
<div class="drag-drop-icon">📸</div>
|
||||
<div class="drag-drop-text">Arraste as imagens aqui ou clique para selecionar</div>
|
||||
<div class="drag-drop-subtext">Suporta arquivos JPG, JPEG e PNG (máx. 10MB por imagem)</div>
|
||||
<input type="file" id="uploadFileInput" multiple accept="image/png, image/jpeg, image/jpg" style="display: none;">
|
||||
</div>
|
||||
|
||||
<!-- Previews das Imagens Selecionadas -->
|
||||
<div class="upload-previews" id="uploadPreviewsContainer"></div>
|
||||
|
||||
<!-- Progresso de Envio -->
|
||||
<div class="upload-progress-container" id="uploadProgressContainer" style="display: none;">
|
||||
<div class="progress-bar-label">
|
||||
<span id="uploadProgressText">Enviando imagens...</span>
|
||||
<span id="uploadProgressPercent">0%</span>
|
||||
</div>
|
||||
<div class="progress-bar-track">
|
||||
<div class="progress-bar-fill" id="uploadProgressBarFill"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions" style="margin-top: 25px; border-top: 1px solid #eee; padding-top: 15px; display: flex; justify-content: flex-end; gap: 10px;">
|
||||
<button type="button" class="btn btn-secondary" onclick="closeSyncModal()">Cancelar</button>
|
||||
<button type="submit" class="btn btn-primary" id="btnSubmitManualUpload" disabled>Enviar Imagens</button>
|
||||
</div>
|
||||
</form>
|
||||
<p style="margin-bottom: 15px; color: #555;">Copie o token abaixo e cole no cliente desktop junto com o email e a senha para autenticar esta clínica.</p>
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<input type="text" id="clinicTokenInput" class="form-control" readonly style="flex: 1; font-family: monospace; font-size: 14px;">
|
||||
<button class="btn btn-primary" onclick="copyTokenToClipboard()">📋 Copiar</button>
|
||||
</div>
|
||||
<p id="tokenCopySuccess" style="color: green; margin-top: 10px; display: none;">✅ Token copiado para a área de transferência!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Application Script -->
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
<script src="/app.js?v=2.7"></script>
|
||||
<script src="/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+15
-451
@@ -705,14 +705,11 @@ body {
|
||||
gap: 12px;
|
||||
height: 100%;
|
||||
align-items: stretch;
|
||||
overflow: hidden;
|
||||
padding: 6px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* ---- HORIZONTAL (paisagem) ---- */
|
||||
.transform-grid.grid-landscape {
|
||||
grid-template-columns: 2fr 1.3fr 1.3fr;
|
||||
grid-template-columns: 2fr 1.3fr 1fr 1fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
}
|
||||
/* Original: col 1, 2 linhas */
|
||||
@@ -721,10 +718,10 @@ body {
|
||||
.transform-grid.grid-landscape .transform-option:nth-child(2) { grid-column: 2; grid-row: 1; }
|
||||
/* Flip V: col 2, linha 2 (empilhado sob Flip H) */
|
||||
.transform-grid.grid-landscape .transform-option:nth-child(3) { grid-column: 2; grid-row: 2; }
|
||||
/* +90°: col 3, linha 1 */
|
||||
.transform-grid.grid-landscape .transform-option:nth-child(4) { grid-column: 3; grid-row: 1; }
|
||||
/* -90°: col 3, linha 2 (empilhado sob +90°) */
|
||||
.transform-grid.grid-landscape .transform-option:nth-child(5) { grid-column: 3; grid-row: 2; }
|
||||
/* +90°: col 3, 2 linhas (lado a lado com -90°) */
|
||||
.transform-grid.grid-landscape .transform-option:nth-child(4) { grid-column: 3; grid-row: 1 / span 2; }
|
||||
/* -90°: col 4, 2 linhas (lado a lado com +90°) */
|
||||
.transform-grid.grid-landscape .transform-option:nth-child(5) { grid-column: 4; grid-row: 1 / span 2; }
|
||||
|
||||
/* ---- VERTICAL (retrato) ---- */
|
||||
.transform-grid.grid-portrait {
|
||||
@@ -757,9 +754,10 @@ body {
|
||||
grid-column: auto !important;
|
||||
grid-row: auto !important;
|
||||
}
|
||||
.selection-mode .transform-image-wrapper {
|
||||
.selection-mode .transform-option img {
|
||||
height: 35vh !important;
|
||||
max-height: 420px;
|
||||
max-height: 400px;
|
||||
object-fit: contain;
|
||||
}
|
||||
/* -90°: col 4, linha 2 (empilhado sob +90°) */
|
||||
.transform-grid.grid-portrait .transform-option:nth-child(5) { grid-column: 4; grid-row: 2; }
|
||||
@@ -788,7 +786,6 @@ body {
|
||||
align-items: center;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
position: relative; /* Para posicionar o ajuste fino absoluto */
|
||||
}
|
||||
|
||||
.transform-option:hover {
|
||||
@@ -801,38 +798,13 @@ body {
|
||||
background: rgba(81, 207, 102, 0.08);
|
||||
}
|
||||
|
||||
/* Novo Wrapper para a Imagem */
|
||||
.transform-image-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 20vh;
|
||||
min-height: 160px;
|
||||
max-height: 320px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
background: #000000;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.transform-image-wrapper img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.transform-option img {
|
||||
max-width: 100%;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
object-fit: contain;
|
||||
border-radius: 4px;
|
||||
transition: transform 0.15s ease-out; /* Transição para ajuste fino suave */
|
||||
}
|
||||
|
||||
/* Evita cortes nas rotações de 90° e -90° causados pelo comportamento de rotação 2D do CSS */
|
||||
.transform-option:nth-child(4) .transform-image-wrapper img,
|
||||
.transform-option:nth-child(5) .transform-image-wrapper img {
|
||||
max-width: 100% !important;
|
||||
max-height: 100% !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
object-fit: contain !important;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.transform-name {
|
||||
@@ -842,99 +814,6 @@ body {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Nova Barra Inferior de Ajuste Fino para o Card Selecionado */
|
||||
#fineTuneContainer {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 5px 8px;
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
box-sizing: border-box;
|
||||
margin-top: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.fine-tune-left-btns,
|
||||
.fine-tune-right-btns {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
#fineTuneContainer button {
|
||||
background: #ffffff;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 12px;
|
||||
padding: 4px 8px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: #495057;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
min-width: 32px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#fineTuneContainer button:hover {
|
||||
background: #e9ecef;
|
||||
border-color: #adb5bd;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
#fineTuneContainer button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* Centralização do Título/Legenda */
|
||||
#fineTuneLabelPlaceholder {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0 5px;
|
||||
min-width: 0; /* Permite truncar se necessário */
|
||||
}
|
||||
|
||||
#fineTuneLabelPlaceholder .transform-name {
|
||||
margin-bottom: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* Botão Reset Flutuante no Canto Superior Direito do Card */
|
||||
#btnResetFineTune {
|
||||
position: absolute !important;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background: rgba(229, 62, 62, 0.9) !important;
|
||||
border: 1px solid #e53e3e !important;
|
||||
color: #ffffff !important;
|
||||
border-radius: 50% !important;
|
||||
width: 28px !important;
|
||||
height: 28px !important;
|
||||
padding: 0 !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
font-size: 0.7rem !important;
|
||||
cursor: pointer;
|
||||
z-index: 100;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
#btnResetFineTune:hover {
|
||||
background: #c53030 !important;
|
||||
border-color: #c53030 !important;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* ================================================================
|
||||
@@ -1057,11 +936,7 @@ body {
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
height: 70px !important;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px !important;
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.15);
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
@@ -1129,11 +1004,6 @@ body {
|
||||
}
|
||||
|
||||
.main-content .header {
|
||||
height: 70px !important;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px !important;
|
||||
border-radius: 0;
|
||||
margin: 0;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.2);
|
||||
@@ -1153,14 +1023,7 @@ body {
|
||||
.app-container .container { display: none; }
|
||||
|
||||
/* Header padding */
|
||||
.main-content .header .header-content {
|
||||
padding: 0 !important;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
flex-direction: row !important;
|
||||
justify-content: space-between !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
.header-content { padding: 14px 20px; }
|
||||
|
||||
/* ---------------------------------------------------------------
|
||||
IMAGES GRID — grid puro sem flex tricks
|
||||
@@ -1209,302 +1072,3 @@ body {
|
||||
#toggleDisabledBtn { font-size: 0; padding: 10px; }
|
||||
#toggleDisabledBtn::before { content: '👁'; font-size: 1.1rem; }
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
SYNC & UPLOAD MODAL STYLES
|
||||
================================================================ */
|
||||
|
||||
.sync-tabs {
|
||||
display: flex;
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
margin-bottom: 20px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.sync-tab-btn {
|
||||
padding: 12px 20px;
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 3px solid transparent;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
.sync-tab-btn:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.sync-tab-btn.active {
|
||||
color: var(--primary-color);
|
||||
border-bottom-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.sync-tab-content {
|
||||
display: none;
|
||||
animation: slideDown 0.25s ease;
|
||||
}
|
||||
|
||||
.sync-tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Área de Drag & Drop */
|
||||
.drag-drop-zone {
|
||||
border: 3px dashed var(--border-color);
|
||||
border-radius: var(--radius);
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
background: #fdfdfd;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.drag-drop-zone:hover,
|
||||
.drag-drop-zone.drag-active {
|
||||
border-color: var(--primary-color);
|
||||
background: rgba(102, 126, 234, 0.04);
|
||||
}
|
||||
|
||||
.drag-drop-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.drag-drop-text {
|
||||
font-size: 1rem;
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.drag-drop-subtext {
|
||||
font-size: 0.82rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Preview de arquivos selecionados */
|
||||
.upload-previews {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.preview-item {
|
||||
position: relative;
|
||||
aspect-ratio: 1;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.preview-item img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.preview-remove {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
background: rgba(220, 53, 69, 0.85);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.preview-remove:hover {
|
||||
background: #dc3545;
|
||||
}
|
||||
|
||||
/* Barra de Progresso */
|
||||
.upload-progress-container {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.progress-bar-label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
color: var(--dark-color);
|
||||
}
|
||||
|
||||
.progress-bar-track {
|
||||
background: #e2e8f0;
|
||||
border-radius: 10px;
|
||||
height: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-bar-fill {
|
||||
background: linear-gradient(90deg, var(--primary-color) 0%, #764ba2 100%);
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
transition: width 0.2s ease;
|
||||
}
|
||||
|
||||
/* Lista de dispositivos online */
|
||||
.sync-device-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.sync-device-card {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.sync-device-card:hover {
|
||||
border-color: var(--primary-color);
|
||||
background: #fff;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.03);
|
||||
}
|
||||
|
||||
.device-card-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.device-card-title {
|
||||
font-weight: 700;
|
||||
font-size: 0.95rem;
|
||||
color: var(--dark-color);
|
||||
}
|
||||
|
||||
.device-card-subtitle {
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.device-card-badge {
|
||||
padding: 5px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.badge-connected {
|
||||
background: rgba(81, 207, 102, 0.15);
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.badge-disconnected {
|
||||
background: rgba(220, 53, 69, 0.1);
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
/* Botão de Excluir Paciente nos cards iniciais */
|
||||
.delete-patient-btn {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background: rgba(220, 53, 69, 0.85);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
z-index: 5;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 1.05rem;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.25);
|
||||
}
|
||||
|
||||
.delete-patient-btn:hover {
|
||||
background: #dc3545;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
Ajuste de Altura Uniforme para o Header da Galeria e da Sidebar (Desktop)
|
||||
================================================================ */
|
||||
@media (min-width: 768px) {
|
||||
.sidebar-header {
|
||||
height: 70px !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
padding: 0 20px !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
.main-content .header {
|
||||
padding: 0 !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
.main-content .header .header-content {
|
||||
height: 70px !important;
|
||||
padding: 0 20px !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: space-between !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Submenu de Clientes na Sidebar */
|
||||
.sidebar-submenu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.submenu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 15px 6px 42px; /* Alinha o texto do submenu abaixo do ícone do menu pai */
|
||||
color: var(--text-color);
|
||||
text-decoration: none;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
border-left: 3px solid transparent;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
margin: 0 15px;
|
||||
}
|
||||
|
||||
.submenu-item:hover {
|
||||
background-color: rgba(102, 126, 234, 0.06);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.submenu-item.active {
|
||||
background-color: rgba(102, 126, 234, 0.1);
|
||||
color: var(--primary-color);
|
||||
border-left-color: var(--primary-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -48,7 +48,10 @@ router.post('/login', async (req, res) => {
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email
|
||||
email: user.email,
|
||||
clinic_name: user.clinic_name,
|
||||
pc_name: user.pc_name,
|
||||
is_admin: user.is_admin
|
||||
}
|
||||
});
|
||||
|
||||
@@ -85,7 +88,10 @@ router.get('/verify', async (req, res) => {
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email
|
||||
email: user.email,
|
||||
clinic_name: user.clinic_name,
|
||||
pc_name: user.pc_name,
|
||||
is_admin: user.is_admin
|
||||
}
|
||||
});
|
||||
|
||||
@@ -109,13 +115,17 @@ router.post('/logout', (req, res) => {
|
||||
// POST /api/auth/create-user - Criar usuário (admin only)
|
||||
// ================================================================
|
||||
|
||||
router.post('/create-user', async (req, res) => {
|
||||
router.post('/create-user', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { username, email, password } = req.body;
|
||||
if (!req.user.is_admin) {
|
||||
return res.status(403).json({ error: 'Apenas administradores podem criar usuários' });
|
||||
}
|
||||
|
||||
const { username, email, password, clinic_name, pc_name } = req.body;
|
||||
|
||||
if (!username || !email || !password) {
|
||||
return res.status(400).json({
|
||||
error: 'Todos os campos são obrigatórios'
|
||||
error: 'Username, email e senha são obrigatórios'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -136,9 +146,9 @@ router.post('/create-user', async (req, res) => {
|
||||
|
||||
// Criar usuário
|
||||
const result = await db.run(
|
||||
`INSERT INTO users (username, email, password_hash, created_at)
|
||||
VALUES (?, ?, ?, NOW())`,
|
||||
[username, email, passwordHash]
|
||||
`INSERT INTO users (username, email, password_hash, clinic_name, pc_name, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, NOW())`,
|
||||
[username, email, passwordHash, clinic_name || null, pc_name || null]
|
||||
);
|
||||
|
||||
res.json({
|
||||
@@ -153,6 +163,54 @@ router.post('/create-user', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ================================================================
|
||||
// GERENCIAMENTO DE USUÁRIOS/CLÍNICAS (ADMIN)
|
||||
// ================================================================
|
||||
|
||||
router.get('/users', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
if (!req.user.is_admin) {
|
||||
return res.status(403).json({ error: 'Acesso negado' });
|
||||
}
|
||||
const users = await db.all('SELECT id, username, email, clinic_name, pc_name, is_admin, created_at FROM users ORDER BY created_at DESC');
|
||||
res.json({ success: true, users });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Erro ao buscar usuários' });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/users/:id', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
if (!req.user.is_admin) {
|
||||
return res.status(403).json({ error: 'Acesso negado' });
|
||||
}
|
||||
const userId = req.params.id;
|
||||
if (parseInt(userId) === req.user.id) {
|
||||
return res.status(400).json({ error: 'Não é possível excluir o próprio usuário admin' });
|
||||
}
|
||||
await db.run('DELETE FROM users WHERE id = ?', [userId]);
|
||||
res.json({ success: true, message: 'Usuário excluído com sucesso' });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Erro ao excluir usuário' });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/users/:id/token', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
if (!req.user.is_admin) {
|
||||
return res.status(403).json({ error: 'Acesso negado' });
|
||||
}
|
||||
const user = await db.get('SELECT * FROM users WHERE id = ?', [req.params.id]);
|
||||
if (!user) return res.status(404).json({ error: 'Usuário não encontrado' });
|
||||
|
||||
// Gera um token com duração longa para uso no desktop (ex: 3650 dias = ~10 anos)
|
||||
const token = generateToken(user, '3650d');
|
||||
res.json({ success: true, token });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Erro ao gerar token' });
|
||||
}
|
||||
});
|
||||
|
||||
// ================================================================
|
||||
// PUT /api/auth/update-credentials - Alterar usuário/senha
|
||||
// ================================================================
|
||||
|
||||
+36
-154
@@ -4,8 +4,6 @@ const db = require('../database');
|
||||
const imageProcessor = require('../image-processor');
|
||||
const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
const storage = require('../storage');
|
||||
const sharp = require('sharp');
|
||||
|
||||
// ================================================================
|
||||
// GET /api/images - Listar imagens
|
||||
@@ -66,12 +64,12 @@ router.get('/patients', async (req, res) => {
|
||||
const rows = await db.all(
|
||||
`SELECT
|
||||
patient_name,
|
||||
MAX(doctor) AS doctor,
|
||||
MAX(remark) AS remark,
|
||||
MAX(client_name) AS client_name,
|
||||
doctor,
|
||||
remark,
|
||||
client_name,
|
||||
COUNT(*) AS image_count,
|
||||
MAX(created_at) AS last_date,
|
||||
(SELECT COALESCE(thumb_filename, filename) FROM images i2
|
||||
(SELECT filename FROM images i2
|
||||
WHERE i2.patient_name = images.patient_name AND i2.enabled = ${enabledVal}
|
||||
ORDER BY i2.created_at DESC LIMIT 1) AS thumb_filename
|
||||
FROM images
|
||||
@@ -116,107 +114,6 @@ router.get('/by-patient', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ================================================================
|
||||
// POST /api/images/upload - Fazer upload manual de imagem
|
||||
// ================================================================
|
||||
router.post('/upload', async (req, res) => {
|
||||
try {
|
||||
const { imageBase64, filename, patientName, doctor, remark } = req.body;
|
||||
|
||||
if (!imageBase64) {
|
||||
return res.status(400).json({ error: 'imageBase64 é obrigatório' });
|
||||
}
|
||||
|
||||
// Decodificar imagem base64
|
||||
let base64Data = imageBase64;
|
||||
if (base64Data.includes(';base64,')) {
|
||||
base64Data = base64Data.split(';base64,')[1];
|
||||
}
|
||||
const imageBuffer = Buffer.from(base64Data, 'base64');
|
||||
|
||||
// TODO(security): Adicionado limite de 10MB para prevenir Buffer Overflow/DoS
|
||||
if (imageBuffer.length > 10 * 1024 * 1024) {
|
||||
return res.status(400).json({ error: 'A imagem excede o limite de tamanho permitido de 10MB.' });
|
||||
}
|
||||
|
||||
// Validar imagem (garante integridade do arquivo através do sharp)
|
||||
await imageProcessor.validateImage(imageBuffer);
|
||||
|
||||
// Salvar imagem
|
||||
const imageGuid = 'web_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
||||
|
||||
// Detectar formato
|
||||
const imgMetadata = await sharp(imageBuffer).metadata();
|
||||
const format = imgMetadata.format || 'png';
|
||||
const ext = format === 'jpeg' ? 'jpg' : format;
|
||||
const savedFilename = `${Date.now()}_${imageGuid}.${ext}`;
|
||||
|
||||
const clientName = 'Upload Web';
|
||||
const patientVal = patientName ? patientName.trim() : 'Paciente não identificado';
|
||||
|
||||
await storage.saveImage(savedFilename, imageBuffer, clientName, patientVal, false);
|
||||
|
||||
// Gerar thumbnail WebP
|
||||
let thumbFilename = null;
|
||||
try {
|
||||
const thumbBuffer = await sharp(imageBuffer)
|
||||
.resize({ width: 400, height: 400, fit: 'inside', withoutEnlargement: true })
|
||||
.webp({ quality: 80, effort: 4 })
|
||||
.toBuffer();
|
||||
|
||||
thumbFilename = `thumb_${Date.now()}_${imageGuid}.webp`;
|
||||
await storage.saveImage(thumbFilename, thumbBuffer, clientName, patientVal, false);
|
||||
console.log(`✅ Thumbnail gerado e salvo para upload manual: ${thumbFilename}`);
|
||||
} catch (thumbErr) {
|
||||
console.error('⚠️ Falha ao gerar thumbnail para upload manual:', thumbErr.message);
|
||||
}
|
||||
|
||||
// Inserir no banco de dados
|
||||
const formatLocalDateTime = (date) => {
|
||||
const pad = (num) => String(num).padStart(2, '0');
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
||||
};
|
||||
const createdAt = formatLocalDateTime(new Date());
|
||||
|
||||
const result = await db.run(
|
||||
`INSERT INTO images (
|
||||
image_guid,
|
||||
patient_name,
|
||||
client_name,
|
||||
original_filename,
|
||||
filename,
|
||||
enabled,
|
||||
doctor,
|
||||
remark,
|
||||
thumb_filename,
|
||||
created_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
imageGuid,
|
||||
patientVal,
|
||||
clientName,
|
||||
filename || 'upload_manual.png',
|
||||
savedFilename,
|
||||
1,
|
||||
doctor || null,
|
||||
remark || null,
|
||||
thumbFilename,
|
||||
createdAt
|
||||
]
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
imageId: result.lastID,
|
||||
filename: savedFilename,
|
||||
message: 'Imagem enviada com sucesso!'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Erro no upload de imagem manual:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ================================================================
|
||||
// GET /api/images/:id - Buscar imagem por ID
|
||||
// ================================================================
|
||||
@@ -267,13 +164,19 @@ router.get('/:id/transformations', async (req, res) => {
|
||||
const originalId = image.original_image_id || image.id;
|
||||
const originalImage = await db.get('SELECT * FROM images WHERE id = ?', [originalId]);
|
||||
|
||||
let originalBuffer = await storage.getImageBuffer(originalImage.filename, false);
|
||||
if (!originalBuffer) {
|
||||
originalBuffer = await storage.getImageBuffer(originalImage.filename, true);
|
||||
}
|
||||
const originalPath = path.join(__dirname, '../uploads', originalImage.filename);
|
||||
let originalBuffer;
|
||||
|
||||
if (!originalBuffer) {
|
||||
return res.status(404).json({ error: 'Arquivo de imagem original não encontrado' });
|
||||
try {
|
||||
originalBuffer = await fs.readFile(originalPath);
|
||||
} catch (e) {
|
||||
if (e.code === 'ENOENT') {
|
||||
// Fallback for images corrupted by previous bug
|
||||
const fallbackPath = path.join(__dirname, '../processed', originalImage.filename);
|
||||
originalBuffer = await fs.readFile(fallbackPath);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
const transformations = [
|
||||
@@ -325,13 +228,17 @@ router.post('/:id/transform', async (req, res) => {
|
||||
const realOriginal = await db.get('SELECT * FROM images WHERE id = ?', [realOriginalId]);
|
||||
|
||||
// Processar a imagem
|
||||
let originalBuffer = await storage.getImageBuffer(realOriginal.filename, false);
|
||||
if (!originalBuffer) {
|
||||
originalBuffer = await storage.getImageBuffer(realOriginal.filename, true);
|
||||
}
|
||||
|
||||
if (!originalBuffer) {
|
||||
return res.status(404).json({ error: 'Arquivo de imagem original não encontrado' });
|
||||
const originalPath = path.join(__dirname, '../uploads', realOriginal.filename);
|
||||
let originalBuffer;
|
||||
try {
|
||||
originalBuffer = await fs.readFile(originalPath);
|
||||
} catch (e) {
|
||||
if (e.code === 'ENOENT') {
|
||||
const fallbackPath = path.join(__dirname, '../processed', realOriginal.filename);
|
||||
originalBuffer = await fs.readFile(fallbackPath);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
let processed = await imageProcessor.rotate(originalBuffer, rotation);
|
||||
@@ -340,7 +247,8 @@ router.post('/:id/transform', async (req, res) => {
|
||||
// Salvar nova versão em processed
|
||||
const timestamp = Date.now();
|
||||
const processedFilename = `${realOriginal.image_guid}_${timestamp}.png`;
|
||||
await storage.saveImage(processedFilename, processed, realOriginal.client_name, realOriginal.patient_name, true);
|
||||
const processedPath = path.join(__dirname, '../processed', processedFilename);
|
||||
await fs.writeFile(processedPath, processed);
|
||||
|
||||
// Desabilitar a imagem atual para que não fique duplicada na interface
|
||||
await db.run('UPDATE images SET enabled = 0 WHERE id = ?', [req.params.id]);
|
||||
@@ -431,37 +339,6 @@ router.get('/:id/download', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ================================================================
|
||||
// DELETE /api/images/by-patient - Apagar todas as imagens de um paciente
|
||||
// ================================================================
|
||||
router.delete('/by-patient', async (req, res) => {
|
||||
try {
|
||||
const patientName = req.query.name || '';
|
||||
if (!patientName) {
|
||||
return res.status(400).json({ error: 'Nome do paciente é obrigatório' });
|
||||
}
|
||||
|
||||
// Buscar todas as imagens do paciente no banco de dados
|
||||
const images = await db.all('SELECT filename, client_name, patient_name FROM images WHERE patient_name = ?', [patientName]);
|
||||
|
||||
// Apagar cada imagem do storage (Wasabi S3 + Local)
|
||||
for (const img of images) {
|
||||
await storage.deleteImage(img.filename, img.client_name, img.patient_name);
|
||||
}
|
||||
|
||||
// Apagar os registros do banco de dados
|
||||
await db.run('DELETE FROM images WHERE patient_name = ?', [patientName]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Todas as imagens do paciente "${patientName}" foram excluídas com sucesso do servidor e do Wasabi.`
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Erro ao excluir imagens do paciente em lote:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ================================================================
|
||||
// DELETE /api/images/:id - Apagar versão da imagem
|
||||
// ================================================================
|
||||
@@ -480,8 +357,13 @@ router.delete('/:id', async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
// Apagar arquivo do storage (Wasabi S3 + Local)
|
||||
await storage.deleteImage(image.filename, image.client_name, image.patient_name);
|
||||
// Apagar arquivo
|
||||
const filePath = path.join(__dirname, '../processed', image.filename);
|
||||
try {
|
||||
await fs.unlink(filePath);
|
||||
} catch (e) {
|
||||
console.warn('Arquivo não encontrado para deletar:', filePath);
|
||||
}
|
||||
|
||||
// Apagar do banco
|
||||
await db.run('DELETE FROM images WHERE id = ?', [req.params.id]);
|
||||
|
||||
@@ -68,29 +68,4 @@ router.delete('/factory-reset', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ================================================================
|
||||
// GET /api/system/storage-config - Obter configurações de storage
|
||||
// ================================================================
|
||||
router.get('/storage-config', (req, res) => {
|
||||
try {
|
||||
const storage = require('../storage');
|
||||
res.json(storage.getSafeConfig());
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ================================================================
|
||||
// POST /api/system/storage-config - Atualizar configurações de storage
|
||||
// ================================================================
|
||||
router.post('/storage-config', async (req, res) => {
|
||||
try {
|
||||
const storage = require('../storage');
|
||||
const result = await storage.updateConfig(req.body);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
+24
-114
@@ -5,7 +5,6 @@ const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const cors = require('cors');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const sharp = require('sharp');
|
||||
require('dotenv').config();
|
||||
|
||||
// Importar módulos
|
||||
@@ -13,7 +12,6 @@ const imageProcessor = require('./image-processor');
|
||||
const { authenticateToken, hashPassword, JWT_SECRET } = require('./auth');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const db = require('./database');
|
||||
const storage = require('./storage');
|
||||
const authRoutes = require('./routes/auth');
|
||||
const imageRoutes = require('./routes/images');
|
||||
const patientsRouter = require('./routes/patients');
|
||||
@@ -328,9 +326,9 @@ app.post('/api/install', async (req, res) => {
|
||||
await db.run(
|
||||
`INSERT INTO users (username, email, password_hash, created_at)
|
||||
VALUES (?, ?, ?, NOW())
|
||||
ON CONFLICT (username) DO UPDATE SET
|
||||
email = EXCLUDED.email,
|
||||
password_hash = EXCLUDED.password_hash`,
|
||||
ON DUPLICATE KEY UPDATE
|
||||
email = VALUES(email),
|
||||
password_hash = VALUES(password_hash)`,
|
||||
[admin.username, admin.email, passwordHash]
|
||||
);
|
||||
console.log('✅ Usuário admin criado:', admin.username);
|
||||
@@ -442,44 +440,8 @@ app.use('/login', express.static('auth'));
|
||||
|
||||
// Serve static files
|
||||
app.use(express.static('public'));
|
||||
|
||||
app.get('/uploads/:filename', async (req, res) => {
|
||||
const filename = req.params.filename;
|
||||
|
||||
// Enviar ETag e Cache-Control imutável
|
||||
res.setHeader('ETag', `"${filename}"`);
|
||||
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
|
||||
|
||||
const clientEtag = req.headers['if-none-match'];
|
||||
if (clientEtag === `"${filename}"` || clientEtag === filename || clientEtag === `W/"${filename}"`) {
|
||||
return res.status(304).end();
|
||||
}
|
||||
|
||||
// Tentar primeiro obter da pasta de uploads (imagens originais)
|
||||
let buffer = await storage.getImageBuffer(filename, false);
|
||||
|
||||
// Se não encontrar, tentar da pasta de processed (imagens rotacionadas/transformadas)
|
||||
if (!buffer) {
|
||||
buffer = await storage.getImageBuffer(filename, true);
|
||||
}
|
||||
|
||||
if (!buffer) {
|
||||
return res.status(404).send('Imagem não encontrada');
|
||||
}
|
||||
|
||||
const ext = path.extname(filename).toLowerCase();
|
||||
let contentType = 'image/png';
|
||||
if (ext === '.jpg' || ext === '.jpeg') {
|
||||
contentType = 'image/jpeg';
|
||||
} else if (ext === '.webp') {
|
||||
contentType = 'image/webp';
|
||||
} else if (ext === '.svg') {
|
||||
contentType = 'image/svg+xml';
|
||||
}
|
||||
|
||||
res.setHeader('Content-Type', contentType);
|
||||
res.send(buffer);
|
||||
});
|
||||
app.use('/uploads', express.static(UPLOAD_DIR));
|
||||
app.use('/uploads', express.static(PROCESSED_DIR));
|
||||
|
||||
// ================================================================
|
||||
// MIDDLEWARE DE VERIFICAÇÃO DE INSTALAÇÃO (APENAS PARA APIs)
|
||||
@@ -534,29 +496,10 @@ app.use('/api/images', imageRoutes);
|
||||
app.use('/api/gtos', authenticateToken);
|
||||
app.use('/api/gtos', gtosRouter);
|
||||
|
||||
// APIs do Sistema - requerem autenticação
|
||||
app.use('/api/system', authenticateToken);
|
||||
app.use('/api/system', systemRoutes);
|
||||
|
||||
// Endpoint para solicitar a sincronização aos dispositivos conectados
|
||||
app.post('/api/system/trigger-sync', authenticateToken, (req, res) => {
|
||||
try {
|
||||
const windowsClients = connectedClients.filter(c => c.type === 'windows');
|
||||
|
||||
// Envia o evento 'sync-request' com timestamp para todos os dispositivos clientes identificados
|
||||
io.emit('sync-request', { timestamp: Date.now() });
|
||||
console.log(`🔄 [Sync] Sinal de sincronização enviado para ${windowsClients.length} dispositivo(s) Windows.`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
devicesTriggered: windowsClients.length,
|
||||
message: `Sinal de sincronização enviado com sucesso para ${windowsClients.length} dispositivo(s) Windows.`
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Erro ao solicitar sincronização:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ================================================================
|
||||
// SOCKET.IO - CONEXÕES EM TEMPO REAL
|
||||
// ================================================================
|
||||
@@ -746,37 +689,16 @@ io.on('connection', (socket) => {
|
||||
// Validar imagem
|
||||
await imageProcessor.validateImage(imageBuffer);
|
||||
|
||||
// Detectar formato da imagem e gerar extensão adequada
|
||||
const imgMetadata = await sharp(imageBuffer).metadata();
|
||||
const format = imgMetadata.format || 'png';
|
||||
const ext = format === 'jpeg' ? 'jpg' : format;
|
||||
|
||||
// Salvar imagem original
|
||||
const timestamp = Date.now();
|
||||
const filename = `${timestamp}_${data.metadata.guid}.${ext}`;
|
||||
const filename = `${timestamp}_${data.metadata.guid}.png`;
|
||||
const filePath = path.join(UPLOAD_DIR, filename);
|
||||
|
||||
await fs.writeFile(filePath, imageBuffer);
|
||||
|
||||
// Buscar nome do cliente se identificado
|
||||
const clientInfo = connectedClients.find(c => c.socketId === socket.id);
|
||||
const clientName = clientInfo ? clientInfo.name : 'Cliente não identificado';
|
||||
const patientName = data.patientData.name || 'Paciente não identificado';
|
||||
|
||||
await storage.saveImage(filename, imageBuffer, clientName, patientName, false);
|
||||
|
||||
// Gerar e salvar thumbnail WebP
|
||||
let thumbFilename = null;
|
||||
try {
|
||||
const thumbBuffer = await sharp(imageBuffer)
|
||||
.resize({ width: 400, height: 400, fit: 'inside', withoutEnlargement: true })
|
||||
.webp({ quality: 80, effort: 4 })
|
||||
.toBuffer();
|
||||
|
||||
thumbFilename = `thumb_${timestamp}_${data.metadata.guid}.webp`;
|
||||
await storage.saveImage(thumbFilename, thumbBuffer, clientName, patientName, false);
|
||||
console.log(`✅ Thumbnail gerado e salvo no Wasabi: ${thumbFilename}`);
|
||||
} catch (thumbErr) {
|
||||
console.error('⚠️ Falha ao gerar thumbnail, prosseguindo sem ele:', thumbErr.message);
|
||||
}
|
||||
|
||||
// Determinar data de criação
|
||||
const formatLocalDateTime = (date) => {
|
||||
@@ -797,9 +719,8 @@ io.on('connection', (socket) => {
|
||||
enabled,
|
||||
doctor,
|
||||
remark,
|
||||
thumb_filename,
|
||||
created_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
data.metadata.guid,
|
||||
data.patientData.name || 'Paciente não identificado',
|
||||
@@ -809,7 +730,6 @@ io.on('connection', (socket) => {
|
||||
1,
|
||||
data.patientData.doctor || null,
|
||||
data.patientData.remark || null,
|
||||
thumbFilename,
|
||||
createdAt
|
||||
]
|
||||
);
|
||||
@@ -841,7 +761,6 @@ io.on('connection', (socket) => {
|
||||
doctor: data.patientData.doctor || null,
|
||||
remark: data.patientData.remark || null,
|
||||
filename: filename,
|
||||
thumb_filename: thumbFilename,
|
||||
created_at: createdAt
|
||||
});
|
||||
}
|
||||
@@ -969,35 +888,26 @@ async function initDirectories() {
|
||||
}
|
||||
|
||||
async function start() {
|
||||
const fsSync = require('fs');
|
||||
try {
|
||||
// Inicializar diretórios
|
||||
await initDirectories();
|
||||
|
||||
// Inicializar banco se as variáveis já existirem no ambiente
|
||||
if (process.env.DB_TYPE === 'sqlite' || (process.env.DB_HOST && process.env.DB_NAME)) {
|
||||
// Tentar inicializar banco apenas se .env já existe
|
||||
const envPath = path.join(__dirname, '.env');
|
||||
const fsSync = require('fs');
|
||||
|
||||
if (fsSync.existsSync(envPath)) {
|
||||
try {
|
||||
await db.initDatabase();
|
||||
console.log('✅ Banco de dados inicializado a partir do ambiente');
|
||||
await storage.loadConfigFromDb();
|
||||
} catch (error) {
|
||||
console.log('❌ Falha ao inicializar o banco de dados:', error.message);
|
||||
}
|
||||
} else {
|
||||
// Fallback para arquivo .env físico
|
||||
const envPath = path.join(__dirname, '.env');
|
||||
const fsSync = require('fs');
|
||||
if (fsSync.existsSync(envPath)) {
|
||||
try {
|
||||
require('dotenv').config();
|
||||
if (process.env.DB_TYPE === 'sqlite' || (process.env.DB_HOST && process.env.DB_NAME)) {
|
||||
await db.initDatabase();
|
||||
console.log('✅ Banco de dados inicializado a partir do .env');
|
||||
await storage.loadConfigFromDb();
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('⚠️ Banco de dados não configurado. Execute a instalação em /install');
|
||||
// Recarregar .env se foi atualizado
|
||||
require('dotenv').config();
|
||||
|
||||
// Inicializar banco de dados apenas se configurações existem
|
||||
if (process.env.DB_TYPE === 'sqlite' || (process.env.DB_HOST && process.env.DB_NAME)) {
|
||||
await db.initDatabase();
|
||||
console.log('✅ Banco de dados inicializado');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('⚠️ Banco de dados não configurado. Execute a instalação em /install');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+76
-88
@@ -79,6 +79,11 @@ array-flatten@1.1.1:
|
||||
resolved "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz"
|
||||
integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==
|
||||
|
||||
aws-ssl-profiles@^1.1.1:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.npmmirror.com/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz"
|
||||
integrity sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==
|
||||
|
||||
b4a@^1.6.4:
|
||||
version "1.7.3"
|
||||
resolved "https://registry.npmmirror.com/b4a/-/b4a-1.7.3.tgz"
|
||||
@@ -371,6 +376,11 @@ delegates@^1.0.0:
|
||||
resolved "https://registry.npmmirror.com/delegates/-/delegates-1.0.0.tgz"
|
||||
integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==
|
||||
|
||||
denque@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.npmmirror.com/denque/-/denque-2.1.0.tgz"
|
||||
integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==
|
||||
|
||||
depd@~2.0.0, depd@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz"
|
||||
@@ -609,6 +619,13 @@ gauge@^3.0.0:
|
||||
strip-ansi "^6.0.1"
|
||||
wide-align "^1.1.2"
|
||||
|
||||
generate-function@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.npmmirror.com/generate-function/-/generate-function-2.3.1.tgz"
|
||||
integrity sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==
|
||||
dependencies:
|
||||
is-property "^1.0.2"
|
||||
|
||||
get-intrinsic@^1.2.5, get-intrinsic@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz"
|
||||
@@ -691,6 +708,13 @@ https-proxy-agent@^5.0.0:
|
||||
agent-base "6"
|
||||
debug "4"
|
||||
|
||||
iconv-lite@^0.7.0:
|
||||
version "0.7.0"
|
||||
resolved "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.0.tgz"
|
||||
integrity sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==
|
||||
dependencies:
|
||||
safer-buffer ">= 2.1.2 < 3.0.0"
|
||||
|
||||
iconv-lite@0.4.24:
|
||||
version "0.4.24"
|
||||
resolved "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz"
|
||||
@@ -736,6 +760,11 @@ is-fullwidth-code-point@^3.0.0:
|
||||
resolved "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz"
|
||||
integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
|
||||
|
||||
is-property@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmmirror.com/is-property/-/is-property-1.0.2.tgz"
|
||||
integrity sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==
|
||||
|
||||
jsonwebtoken@^9.0.2:
|
||||
version "9.0.2"
|
||||
resolved "https://registry.npmmirror.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz"
|
||||
@@ -804,6 +833,21 @@ lodash.once@^4.0.0:
|
||||
resolved "https://registry.npmmirror.com/lodash.once/-/lodash.once-4.1.1.tgz"
|
||||
integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==
|
||||
|
||||
long@^5.2.1:
|
||||
version "5.3.2"
|
||||
resolved "https://registry.npmmirror.com/long/-/long-5.3.2.tgz"
|
||||
integrity sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==
|
||||
|
||||
lru-cache@^7.14.1:
|
||||
version "7.18.3"
|
||||
resolved "https://registry.npmmirror.com/lru-cache/-/lru-cache-7.18.3.tgz"
|
||||
integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==
|
||||
|
||||
lru.min@^1.0.0:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.npmmirror.com/lru.min/-/lru.min-1.1.2.tgz"
|
||||
integrity sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==
|
||||
|
||||
make-dir@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.npmmirror.com/make-dir/-/make-dir-3.1.0.tgz"
|
||||
@@ -915,6 +959,28 @@ ms@2.1.3:
|
||||
resolved "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz"
|
||||
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
||||
|
||||
mysql2@^3.6.0:
|
||||
version "3.15.3"
|
||||
resolved "https://registry.npmmirror.com/mysql2/-/mysql2-3.15.3.tgz"
|
||||
integrity sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==
|
||||
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"
|
||||
|
||||
named-placeholders@^1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.npmmirror.com/named-placeholders/-/named-placeholders-1.1.3.tgz"
|
||||
integrity sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==
|
||||
dependencies:
|
||||
lru-cache "^7.14.1"
|
||||
|
||||
napi-build-utils@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.npmmirror.com/napi-build-utils/-/napi-build-utils-2.0.0.tgz"
|
||||
@@ -1010,84 +1076,6 @@ path-to-regexp@0.1.12:
|
||||
resolved "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz"
|
||||
integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==
|
||||
|
||||
pg-cloudflare@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz"
|
||||
integrity sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==
|
||||
|
||||
pg-connection-string@^2.13.0:
|
||||
version "2.13.0"
|
||||
resolved "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.13.0.tgz"
|
||||
integrity sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig==
|
||||
|
||||
pg-int8@1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz"
|
||||
integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==
|
||||
|
||||
pg-pool@^3.14.0:
|
||||
version "3.14.0"
|
||||
resolved "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz"
|
||||
integrity sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==
|
||||
|
||||
pg-protocol@^1.14.0:
|
||||
version "1.14.0"
|
||||
resolved "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.14.0.tgz"
|
||||
integrity sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA==
|
||||
|
||||
pg-types@2.2.0:
|
||||
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"
|
||||
|
||||
pg@^8.11.3, pg@>=8.0:
|
||||
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"
|
||||
optionalDependencies:
|
||||
pg-cloudflare "^1.4.0"
|
||||
|
||||
pgpass@1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz"
|
||||
integrity sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==
|
||||
dependencies:
|
||||
split2 "^4.1.0"
|
||||
|
||||
postgres-array@~2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz"
|
||||
integrity sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==
|
||||
|
||||
postgres-bytea@~1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz"
|
||||
integrity sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==
|
||||
|
||||
postgres-date@~1.0.4:
|
||||
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==
|
||||
|
||||
postgres-interval@^1.1.0:
|
||||
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"
|
||||
|
||||
prebuild-install@^7.1.1:
|
||||
version "7.1.3"
|
||||
resolved "https://registry.npmmirror.com/prebuild-install/-/prebuild-install-7.1.3.tgz"
|
||||
@@ -1180,7 +1168,7 @@ safe-buffer@^5.0.1, safe-buffer@~5.2.0, safe-buffer@5.2.1:
|
||||
resolved "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz"
|
||||
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
|
||||
|
||||
"safer-buffer@>= 2.1.2 < 3":
|
||||
"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0":
|
||||
version "2.1.2"
|
||||
resolved "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz"
|
||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||
@@ -1214,6 +1202,11 @@ send@0.19.0:
|
||||
range-parser "~1.2.1"
|
||||
statuses "2.0.1"
|
||||
|
||||
seq-queue@^0.0.5:
|
||||
version "0.0.5"
|
||||
resolved "https://registry.npmmirror.com/seq-queue/-/seq-queue-0.0.5.tgz"
|
||||
integrity sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==
|
||||
|
||||
serve-static@1.16.2:
|
||||
version "1.16.2"
|
||||
resolved "https://registry.npmmirror.com/serve-static/-/serve-static-1.16.2.tgz"
|
||||
@@ -1343,10 +1336,10 @@ socket.io@^4.7.2:
|
||||
socket.io-adapter "~2.5.2"
|
||||
socket.io-parser "~4.2.4"
|
||||
|
||||
split2@^4.1.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz"
|
||||
integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==
|
||||
sqlstring@^2.3.2:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.npmmirror.com/sqlstring/-/sqlstring-2.3.3.tgz"
|
||||
integrity sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==
|
||||
|
||||
statuses@2.0.1:
|
||||
version "2.0.1"
|
||||
@@ -1537,11 +1530,6 @@ ws@~8.17.1:
|
||||
resolved "https://registry.npmmirror.com/ws/-/ws-8.17.1.tgz"
|
||||
integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==
|
||||
|
||||
xtend@^4.0.0:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz"
|
||||
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
|
||||
|
||||
yallist@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz"
|
||||
|
||||
Reference in New Issue
Block a user