21f33c88a4
continuous-integration/webhook Deploy concluído (VPS4)
- Adiciona coluna clinic_name em images (ALTER TABLE IF NOT EXISTS) - Todas as funções de storage recebem clinicName como parâmetro: saveImage, getImageBuffer, getImageUrl, deleteImage, getDownloadPresignedUrl, getUploadPresignedUrl - Chave Wasabi passa de pc_name/patient/arquivo → clinic_name/pc_name/patient/arquivo - server.js: usa socket.user.clinicName (preenchido via machine_token auth do DB) como fonte primária do nome da clínica nos 3 handlers de upload e na rota thumbnail - Todos os INSERTs em images passam a gravar clinic_name - Corrige bug oculto: wipe de clínica agora apaga o prefixo correto no Wasabi - Scripts utilitários atualizados: backfill-wasabi-thumbs, recreate-all-thumbs, migrate-to-wasabi - client-monitor.js: envia clinicName no client-identify (fallback para clientes legados) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
251 lines
8.2 KiB
JavaScript
251 lines
8.2 KiB
JavaScript
const { Pool } = require('pg');
|
|
require('dotenv').config();
|
|
|
|
let pool = null;
|
|
|
|
// ================================================================
|
|
// INICIALIZAR BANCO DE DADOS POSTGRESQL
|
|
// ================================================================
|
|
|
|
async function initDatabase() {
|
|
const dbConfig = {
|
|
host: process.env.DB_HOST || '10.99.0.3',
|
|
port: process.env.DB_PORT || 5432,
|
|
user: process.env.DB_USER || 'postgres',
|
|
database: process.env.DB_NAME || 'dental_images',
|
|
max: 20, // Max number of clients in the pool
|
|
idleTimeoutMillis: 30000,
|
|
};
|
|
|
|
if (process.env.DB_PASSWORD) {
|
|
dbConfig.password = process.env.DB_PASSWORD;
|
|
}
|
|
|
|
try {
|
|
pool = new Pool(dbConfig);
|
|
|
|
// Testa a conexão
|
|
const client = await pool.connect();
|
|
console.log(`✅ Conectado ao PostgreSQL (Host: ${dbConfig.host}, DB: ${dbConfig.database})`);
|
|
client.release();
|
|
|
|
// Criar as tabelas
|
|
await createTables();
|
|
} catch (error) {
|
|
console.error('❌ Erro ao conectar ao PostgreSQL:', error.message);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// ================================================================
|
|
// CRIAR TABELAS POSTGRESQL
|
|
// ================================================================
|
|
|
|
async function createTables() {
|
|
const client = await pool.connect();
|
|
|
|
try {
|
|
// Tabela de usuários
|
|
await client.query(`
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id SERIAL PRIMARY KEY,
|
|
username VARCHAR(50) UNIQUE NOT NULL,
|
|
email VARCHAR(100) UNIQUE NOT NULL,
|
|
password_hash TEXT NOT NULL,
|
|
clinic_name VARCHAR(100),
|
|
pc_name VARCHAR(50),
|
|
is_admin BOOLEAN DEFAULT false,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
`);
|
|
console.log('✅ Tabela users criada/verificada no PostgreSQL');
|
|
|
|
// Tabela de imagens
|
|
await client.query(`
|
|
CREATE TABLE IF NOT EXISTS images (
|
|
id SERIAL PRIMARY KEY,
|
|
image_guid VARCHAR(255) NOT NULL,
|
|
patient_name TEXT,
|
|
client_name VARCHAR(100),
|
|
original_filename TEXT,
|
|
filename TEXT NOT NULL,
|
|
enabled BOOLEAN DEFAULT true,
|
|
tooth_number VARCHAR(10),
|
|
tooth_side VARCHAR(10),
|
|
patient_name_resumed VARCHAR(50),
|
|
original_image_id INT,
|
|
rotation INT DEFAULT 0,
|
|
flip_horizontal INT DEFAULT 0,
|
|
flip_vertical INT DEFAULT 0,
|
|
doctor VARCHAR(255),
|
|
remark TEXT,
|
|
thumb_filename TEXT,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
`);
|
|
|
|
// Criação de índices no PostgreSQL
|
|
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_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)');
|
|
|
|
// Garantir que a coluna exista caso a tabela já tenha sido criada antes
|
|
await client.query('ALTER TABLE images ADD COLUMN IF NOT EXISTS thumb_filename TEXT;');
|
|
await client.query('ALTER TABLE images ADD COLUMN IF NOT EXISTS clinic_name VARCHAR(100);');
|
|
|
|
console.log('✅ Tabela images criada/verificada no PostgreSQL');
|
|
|
|
// Tabela GTOs
|
|
await client.query(`
|
|
CREATE TABLE IF NOT EXISTS gtos (
|
|
id SERIAL PRIMARY KEY,
|
|
gto_number VARCHAR(100) NOT NULL,
|
|
patient_name VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
`);
|
|
await client.query('CREATE INDEX IF NOT EXISTS idx_gtos_patient ON gtos(patient_name)');
|
|
await client.query('ALTER TABLE gtos ADD COLUMN IF NOT EXISTS sent BOOLEAN DEFAULT FALSE;');
|
|
await client.query('ALTER TABLE gtos ADD COLUMN IF NOT EXISTS sent_at TIMESTAMP;');
|
|
console.log('✅ Tabela gtos criada/verificada no PostgreSQL');
|
|
|
|
// Tabela de vínculo GTO-Images
|
|
await client.query(`
|
|
CREATE TABLE IF NOT EXISTS gto_images (
|
|
id SERIAL PRIMARY KEY,
|
|
gto_id INT NOT NULL REFERENCES gtos(id) ON DELETE CASCADE,
|
|
image_id INT NOT NULL REFERENCES images(id) ON DELETE CASCADE,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
UNIQUE(gto_id, image_id)
|
|
)
|
|
`);
|
|
console.log('✅ Tabela gto_images criada/verificada no PostgreSQL');
|
|
|
|
// Tabela de Clínicas / Dispositivos
|
|
await client.query(`
|
|
CREATE TABLE IF NOT EXISTS clinics_devices (
|
|
id SERIAL PRIMARY KEY,
|
|
clinic_name VARCHAR(255) NOT NULL,
|
|
email VARCHAR(255) UNIQUE NOT NULL,
|
|
password_hash TEXT NOT NULL,
|
|
pc_name VARCHAR(100),
|
|
machine_token UUID NOT NULL UNIQUE,
|
|
last_ip VARCHAR(50),
|
|
is_active BOOLEAN DEFAULT true,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
`);
|
|
console.log('✅ Tabela clinics_devices criada/verificada no PostgreSQL');
|
|
|
|
// Tabela de configurações do sistema (key-value)
|
|
await client.query(`
|
|
CREATE TABLE IF NOT EXISTS system_config (
|
|
key VARCHAR(100) PRIMARY KEY,
|
|
value TEXT,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
`);
|
|
console.log('✅ Tabela system_config criada/verificada no PostgreSQL');
|
|
|
|
// Verificar colunas faltantes em users (migração)
|
|
try {
|
|
const res = await client.query(`
|
|
SELECT column_name
|
|
FROM information_schema.columns
|
|
WHERE table_name = 'users' AND column_name = 'clinic_name'
|
|
`);
|
|
if (res.rows.length === 0) {
|
|
console.log('⚠️ Adicionando novas colunas na tabela users...');
|
|
await client.query(`
|
|
ALTER TABLE users
|
|
ADD COLUMN clinic_name VARCHAR(100),
|
|
ADD COLUMN pc_name VARCHAR(50),
|
|
ADD COLUMN is_admin BOOLEAN DEFAULT false
|
|
`);
|
|
}
|
|
} catch (e) {
|
|
console.warn('⚠️ Erro ao migrar tabela users:', e.message);
|
|
}
|
|
|
|
// Criar administrador padrão se não houver usuários
|
|
try {
|
|
const res = await client.query('SELECT COUNT(*) as count FROM users');
|
|
if (parseInt(res.rows[0].count) === 0) {
|
|
console.log('⚠️ Nenhum usuário encontrado. Criando admin padrão (admin / admin1234)...');
|
|
const bcrypt = require('bcrypt');
|
|
const passwordHash = bcrypt.hashSync('admin1234', 10);
|
|
await client.query(`
|
|
INSERT INTO users (username, email, password_hash, clinic_name, pc_name, is_admin)
|
|
VALUES ($1, $2, $3, $4, $5, true)
|
|
`, ['admin', 'admin@dental.local', passwordHash, 'Admin Clinic', 'Admin PC']);
|
|
console.log('✅ Usuário admin padrão criado com sucesso!');
|
|
}
|
|
} catch (error) {
|
|
console.error('⚠️ Erro ao criar admin padrão:', error.message);
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('❌ Erro ao criar tabelas no PostgreSQL:', error);
|
|
} finally {
|
|
client.release();
|
|
}
|
|
}
|
|
|
|
// ================================================================
|
|
// FUNÇÕES DE CONSULTA (ENVOLVENDO O POOL DO PG)
|
|
// ================================================================
|
|
|
|
function get(query, params = []) {
|
|
return new Promise(async (resolve, reject) => {
|
|
try {
|
|
const res = await pool.query(query, params);
|
|
resolve(res.rows[0] || null);
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
});
|
|
}
|
|
|
|
function all(query, params = []) {
|
|
return new Promise(async (resolve, reject) => {
|
|
try {
|
|
const res = await pool.query(query, params);
|
|
resolve(res.rows || []);
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
});
|
|
}
|
|
|
|
function run(query, params = []) {
|
|
return new Promise(async (resolve, reject) => {
|
|
try {
|
|
const res = await pool.query(query, params);
|
|
|
|
// No PostgreSQL, se adicionamos RETURNING id, o id estará no res.rows[0]
|
|
const insertedId = (res.rows && res.rows[0] && res.rows[0].id) ? res.rows[0].id : null;
|
|
|
|
resolve({
|
|
lastID: insertedId,
|
|
changes: res.rowCount
|
|
});
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
});
|
|
}
|
|
|
|
// ================================================================
|
|
// EXPORT
|
|
// ================================================================
|
|
|
|
module.exports = {
|
|
initDatabase,
|
|
get,
|
|
all,
|
|
run,
|
|
get db() { return pool; }
|
|
}; |