b6da56617a
Hierarquia tenant_id (usuarios.id do dono) / clinica_id / pc / paciente, espelhando o scoreodonto (conta multi-clínica planejada: um dono gerencia várias clínicas). - Colunas tenant_id em clinics_devices e images (+índices); backfill device + 2776 imgs. - Cadastro de device, socket (machine_token), JWT de client e uploads gravam tenant_id. - Listagens (/images, /patients, /by-patient, /unique-clinics) filtram por tenant_id e/ou clinica_id. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
277 lines
9.9 KiB
JavaScript
277 lines
9.9 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);');
|
|
await client.query('ALTER TABLE images ADD COLUMN IF NOT EXISTS image_remark TEXT;');
|
|
// Multi-tenant (2 níveis, espelha o scoreodonto):
|
|
// tenant_id = conta/dono (usuarios.id "u_<ts>_<rand>") — agrupa várias clínicas.
|
|
// clinica_id = clínica (clinicas.id "c_<ts>_<rand>") — unidade operacional.
|
|
// clinic_name vira só rótulo legível. Hierarquia: tenant_id / clinica_id / pc / paciente.
|
|
await client.query('ALTER TABLE images ADD COLUMN IF NOT EXISTS clinica_id VARCHAR(64);');
|
|
await client.query('ALTER TABLE images ADD COLUMN IF NOT EXISTS tenant_id VARCHAR(64);');
|
|
await client.query('CREATE INDEX IF NOT EXISTS idx_images_clinica_id ON images(clinica_id);');
|
|
await client.query('CREATE INDEX IF NOT EXISTS idx_images_tenant_id ON images(tenant_id);');
|
|
|
|
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
|
|
)
|
|
`);
|
|
// Multi-tenant: device vinculado à clínica (clinica_id) e à conta/dono (tenant_id).
|
|
await client.query('ALTER TABLE clinics_devices ADD COLUMN IF NOT EXISTS clinica_id VARCHAR(64);');
|
|
await client.query('ALTER TABLE clinics_devices ADD COLUMN IF NOT EXISTS tenant_id VARCHAR(64);');
|
|
await client.query('CREATE INDEX IF NOT EXISTS idx_devices_clinica_id ON clinics_devices(clinica_id);');
|
|
await client.query('CREATE INDEX IF NOT EXISTS idx_devices_tenant_id ON clinics_devices(tenant_id);');
|
|
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');
|
|
|
|
// Tabela de Tokens de API
|
|
await client.query(`
|
|
CREATE TABLE IF NOT EXISTS api_tokens (
|
|
id SERIAL PRIMARY KEY,
|
|
name VARCHAR(100) NOT NULL,
|
|
token VARCHAR(255) UNIQUE NOT NULL,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
last_used_at TIMESTAMP
|
|
)
|
|
`);
|
|
console.log('✅ Tabela api_tokens 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; }
|
|
}; |