diff --git a/dental-server/database.js b/dental-server/database.js index d4559a5..ef804b7 100644 --- a/dental-server/database.js +++ b/dental-server/database.js @@ -95,10 +95,14 @@ async function createTables() { 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: clinica_id = tenant do scoreodonto (clinicas.id, ex. "c__"). - // clinic_name vira só rótulo legível; clinica_id é o eixo de isolamento. + // Multi-tenant (2 níveis, espelha o scoreodonto): + // tenant_id = conta/dono (usuarios.id "u__") — agrupa várias clínicas. + // clinica_id = clínica (clinicas.id "c__") — 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'); @@ -143,9 +147,11 @@ async function createTables() { created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); - // Multi-tenant: vincula o device ao tenant do scoreodonto (clinicas.id). + // 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) diff --git a/dental-server/routes/admin-clinics.js b/dental-server/routes/admin-clinics.js index 3f69f93..70ab48d 100644 --- a/dental-server/routes/admin-clinics.js +++ b/dental-server/routes/admin-clinics.js @@ -19,14 +19,15 @@ const requireAdmin = (req, res, next) => { // ============================================================== router.get('/', requireAdmin, async (req, res) => { try { - // Filtro multi-tenant opcional: ?clinica_id=. - const { clinica_id } = req.query; + // Filtros multi-tenant opcionais: ?tenant_id= e/ou ?clinica_id=. + const { clinica_id, tenant_id } = req.query; const clinics = await db.all(` - SELECT id, clinic_name, clinica_id, email, pc_name, machine_token, last_ip, is_active, created_at + SELECT id, clinic_name, clinica_id, tenant_id, email, pc_name, machine_token, last_ip, is_active, created_at FROM clinics_devices - WHERE ($1 = '' OR clinica_id = $1) + WHERE ($1 = '' OR tenant_id = $1) + AND ($2 = '' OR clinica_id = $2) ORDER BY created_at DESC - `, [clinica_id || '']); + `, [tenant_id || '', clinica_id || '']); res.json({ success: true, clinics }); } catch (error) { console.error('Erro ao listar clínicas:', error); @@ -38,7 +39,7 @@ router.get('/', requireAdmin, async (req, res) => { // 2. CADASTRAR CLÍNICA E GERAR TOKEN (ADMIN) // ============================================================== router.post('/', requireAdmin, async (req, res) => { - const { clinic_name, email, password, pc_name, clinica_id } = req.body; + const { clinic_name, email, password, pc_name, clinica_id, tenant_id } = req.body; if (!clinic_name || !email || !password) { return res.status(400).json({ success: false, message: 'Preencha todos os campos obrigatórios' }); @@ -71,9 +72,9 @@ router.post('/', requireAdmin, async (req, res) => { const machine_token = crypto.randomUUID(); await db.run(` - INSERT INTO clinics_devices (clinic_name, email, password_hash, pc_name, machine_token, clinica_id) - VALUES ($1, $2, $3, $4, $5, $6) RETURNING id - `, [clinic_name, email, password_hash, pc_name, machine_token, clinica_id || null]); + INSERT INTO clinics_devices (clinic_name, email, password_hash, pc_name, machine_token, clinica_id, tenant_id) + VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id + `, [clinic_name, email, password_hash, pc_name, machine_token, clinica_id || null, tenant_id || null]); res.json({ success: true, diff --git a/dental-server/routes/client-auth.js b/dental-server/routes/client-auth.js index 904d0c4..1e88fb1 100644 --- a/dental-server/routes/client-auth.js +++ b/dental-server/routes/client-auth.js @@ -51,7 +51,8 @@ router.post('/login', async (req, res) => { const sessionToken = jwt.sign( { clinicId: device.id, - clinicaId: device.clinica_id, // tenant do scoreodonto + tenantId: device.tenant_id, // conta/dono (scoreodonto) + clinicaId: device.clinica_id, // clínica (scoreodonto) clinicName: device.clinic_name, pcName: device.pc_name, role: 'client_device' diff --git a/dental-server/routes/images.js b/dental-server/routes/images.js index 41525e5..3b7ff14 100644 --- a/dental-server/routes/images.js +++ b/dental-server/routes/images.js @@ -53,19 +53,24 @@ router.get('/unique-clients', async (req, res) => { }); // ================================================================ -// GET /api/images/unique-clinics - Tenants (clinica_id) com contagem + rótulo +// GET /api/images/unique-clinics - Clínicas (clinica_id) de uma conta (tenant_id), +// com rótulo e contagens. ?tenant_id= filtra; sem ele, todas. // ================================================================ router.get('/unique-clinics', async (req, res) => { try { + const tenantId = req.query.tenant_id || ''; const rows = await db.all( - `SELECT clinica_id, + `SELECT tenant_id, + clinica_id, MAX(clinic_name) AS clinic_name, COUNT(*)::integer AS image_count, COUNT(DISTINCT patient_name)::integer AS patient_count FROM images WHERE clinica_id IS NOT NULL AND enabled = 1 - GROUP BY clinica_id - ORDER BY clinic_name` + AND ($1 = '' OR tenant_id = $1) + GROUP BY tenant_id, clinica_id + ORDER BY clinic_name`, + [tenantId] ); res.json(rows); } catch (error) { @@ -254,13 +259,19 @@ router.get('/', async (req, res) => { const showDisabled = req.query.disabled === 'true'; const search = req.query.search || ''; const clientName = req.query.client || ''; - const clinicaId = req.query.clinica_id || ''; // tenant do scoreodonto (isolamento) + const tenantId = req.query.tenant_id || ''; // conta/dono (isolamento por conta) + const clinicaId = req.query.clinica_id || ''; // clínica (isolamento por unidade) const clinicName = req.query.clinic || ''; // rótulo legível (filtro opcional) let query = `SELECT * FROM images WHERE enabled = $1`; let params = [showDisabled ? 0 : 1]; let paramIndex = 2; + if (tenantId) { + query += ` AND tenant_id = $${paramIndex++}`; + params.push(tenantId); + } + if (clinicaId) { query += ` AND clinica_id = $${paramIndex++}`; params.push(clinicaId); @@ -300,7 +311,8 @@ router.get('/patients', async (req, res) => { const showDisabled = req.query.disabled === 'true'; const search = req.query.search || ''; const clientName = req.query.client || ''; - const clinicaId = req.query.clinica_id || ''; // tenant do scoreodonto (isolamento) + const tenantId = req.query.tenant_id || ''; // conta/dono (isolamento por conta) + const clinicaId = req.query.clinica_id || ''; // clínica (isolamento por unidade) const clinicName = req.query.clinic || ''; // rótulo legível (filtro opcional) const enabledVal = showDisabled ? 0 : 1; @@ -313,6 +325,11 @@ router.get('/patients', async (req, res) => { const offset = (page - 1) * limit; let whereStr = `WHERE 1=1`; + if (tenantId) { + whereStr += ` AND tenant_id = $${paramIndex}`; + params.push(tenantId); + paramIndex++; + } if (clinicaId) { whereStr += ` AND clinica_id = $${paramIndex}`; params.push(clinicaId); @@ -398,7 +415,8 @@ router.get('/by-patient', async (req, res) => { try { const patientName = req.query.name || ''; const showDisabled = req.query.disabled === 'true'; - const clinicaId = req.query.clinica_id || ''; // tenant do scoreodonto (isolamento) + const tenantId = req.query.tenant_id || ''; // conta/dono (isolamento por conta) + const clinicaId = req.query.clinica_id || ''; // clínica (isolamento por unidade) if (!patientName) { return res.status(400).json({ error: 'Parâmetro name obrigatório' }); @@ -406,6 +424,10 @@ router.get('/by-patient', async (req, res) => { let sql = `SELECT * FROM images WHERE patient_name = $1 AND enabled = $2`; const params = [patientName, showDisabled ? 0 : 1]; + if (tenantId) { + sql += ` AND tenant_id = $${params.length + 1}`; + params.push(tenantId); + } if (clinicaId) { sql += ` AND clinica_id = $${params.length + 1}`; params.push(clinicaId); diff --git a/dental-server/server.js b/dental-server/server.js index 40aa704..26ceb32 100644 --- a/dental-server/server.js +++ b/dental-server/server.js @@ -896,7 +896,7 @@ io.use(async (socket, next) => { // machine_token válido (com ou sem email+senha) console.log(`✅ [machine_token] Conexão autorizada — Clínica: ${device.clinic_name} PC: ${device.pc_name}`); socket.clientType = 'sensor'; - socket.user = { role: 'client_device', clinicId: device.id, clinicaId: device.clinica_id, clinicName: device.clinic_name, pcName: device.pc_name }; + socket.user = { role: 'client_device', clinicId: device.id, tenantId: device.tenant_id, clinicaId: device.clinica_id, clinicName: device.clinic_name, pcName: device.pc_name }; await db.run(`UPDATE clinics_devices SET last_ip = $1 WHERE id = $2`, [socket.handshake.address, device.id]); return next(); } @@ -1117,7 +1117,8 @@ io.on('connection', (socket) => { const clientInfo = connectedClients.find(c => c.socketId === socket.id); const clientName = clientInfo ? clientInfo.name : 'PC não identificado'; const clinicName = socket.user?.clinicName || clientInfo?.clinicName || 'Clínica não identificada'; - const clinicaId = socket.user?.clinicaId || clientInfo?.clinicaId || null; // tenant do scoreodonto + const clinicaId = socket.user?.clinicaId || clientInfo?.clinicaId || null; // clínica (scoreodonto) + const tenantId = socket.user?.tenantId || clientInfo?.tenantId || null; // conta/dono (scoreodonto) const patientName = (data.patientData && data.patientData.name && data.patientData.name.trim()) ? data.patientData.name.trim() : 'Desconhecido'; // Salvar imagem original local e na nuvem @@ -1151,6 +1152,7 @@ io.on('connection', (socket) => { patient_name, clinic_name, clinica_id, + tenant_id, client_name, original_filename, filename, @@ -1159,12 +1161,13 @@ io.on('connection', (socket) => { remark, thumb_filename, created_at - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING id`, + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING id`, [ data.metadata.guid, (data.patientData.name && data.patientData.name.trim()) ? data.patientData.name.trim() : 'Desconhecido', clinicName, clinicaId, + tenantId, clientName, data.metadata.fileName, filename, @@ -1269,7 +1272,8 @@ io.on('connection', (socket) => { const clientInfo = connectedClients.find(c => c.socketId === socket.id); const clientName = clientInfo ? clientInfo.name : 'PC não identificado'; const clinicName = socket.user?.clinicName || clientInfo?.clinicName || 'Clínica não identificada'; - const clinicaId = socket.user?.clinicaId || clientInfo?.clinicaId || null; // tenant do scoreodonto + const clinicaId = socket.user?.clinicaId || clientInfo?.clinicaId || null; // clínica (scoreodonto) + const tenantId = socket.user?.tenantId || clientInfo?.tenantId || null; // conta/dono (scoreodonto) // Determinar data de criação const formatLocalDateTime = (date) => { @@ -1286,6 +1290,7 @@ io.on('connection', (socket) => { patient_name, clinic_name, clinica_id, + tenant_id, client_name, original_filename, filename, @@ -1294,12 +1299,13 @@ io.on('connection', (socket) => { remark, thumb_filename, created_at - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING id`, + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING id`, [ data.metadata.guid, (data.patientData.name && data.patientData.name.trim()) ? data.patientData.name.trim() : 'Desconhecido', clinicName, clinicaId, + tenantId, clientName, data.metadata.fileName, data.filename,