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>
211 lines
8.3 KiB
JavaScript
211 lines
8.3 KiB
JavaScript
const express = require('express');
|
|
const router = express.Router();
|
|
const db = require('../database');
|
|
const crypto = require('crypto');
|
|
const bcrypt = require('bcrypt');
|
|
const storage = require('../storage');
|
|
const { S3Client, ListObjectsV2Command, DeleteObjectsCommand } = require('@aws-sdk/client-s3');
|
|
|
|
// Middleware para verificar se é admin
|
|
const requireAdmin = (req, res, next) => {
|
|
if (req.user && req.user.is_admin) {
|
|
return next();
|
|
}
|
|
return res.status(403).json({ success: false, message: 'Acesso negado' });
|
|
};
|
|
|
|
// ==============================================================
|
|
// 1. LISTAR CLÍNICAS (ADMIN)
|
|
// ==============================================================
|
|
router.get('/', requireAdmin, async (req, res) => {
|
|
try {
|
|
// Filtros multi-tenant opcionais: ?tenant_id=<conta> e/ou ?clinica_id=<clínica>.
|
|
const { clinica_id, tenant_id } = req.query;
|
|
const clinics = await db.all(`
|
|
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 tenant_id = $1)
|
|
AND ($2 = '' OR clinica_id = $2)
|
|
ORDER BY created_at DESC
|
|
`, [tenant_id || '', clinica_id || '']);
|
|
res.json({ success: true, clinics });
|
|
} catch (error) {
|
|
console.error('Erro ao listar clínicas:', error);
|
|
res.status(500).json({ success: false, message: 'Erro interno do servidor' });
|
|
}
|
|
});
|
|
|
|
// ==============================================================
|
|
// 2. CADASTRAR CLÍNICA E GERAR TOKEN (ADMIN)
|
|
// ==============================================================
|
|
router.post('/', requireAdmin, async (req, res) => {
|
|
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' });
|
|
}
|
|
|
|
try {
|
|
// Verificar se email já existe
|
|
const existing = await db.get(`SELECT id FROM clinics_devices WHERE email = $1`, [email]);
|
|
if (existing) {
|
|
return res.status(400).json({ success: false, message: 'Email já cadastrado para outra clínica' });
|
|
}
|
|
|
|
// Verificar se já existe um dispositivo com mesmo clinic_name + pc_name
|
|
// (evita dois PCs com caminho idêntico no Wasabi, sobrescrevendo arquivos um do outro)
|
|
if (pc_name) {
|
|
const dupPath = await db.get(
|
|
`SELECT id FROM clinics_devices WHERE LOWER(clinic_name) = LOWER($1) AND LOWER(pc_name) = LOWER($2)`,
|
|
[clinic_name, pc_name]
|
|
);
|
|
if (dupPath) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: `Já existe um dispositivo com a clínica "${clinic_name}" e PC "${pc_name}". Use um nome de PC diferente.`
|
|
});
|
|
}
|
|
}
|
|
|
|
// Gerar hash da senha e token UUID da máquina
|
|
const password_hash = bcrypt.hashSync(password, 10);
|
|
const machine_token = crypto.randomUUID();
|
|
|
|
await db.run(`
|
|
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,
|
|
message: 'Clínica cadastrada com sucesso!',
|
|
machine_token
|
|
});
|
|
} catch (error) {
|
|
console.error('Erro ao cadastrar clínica:', error);
|
|
res.status(500).json({ success: false, message: 'Erro interno ao cadastrar' });
|
|
}
|
|
});
|
|
|
|
// ==============================================================
|
|
// 3. ALTERAR STATUS DA CLÍNICA (ATIVAR/DESATIVAR) (ADMIN)
|
|
// ==============================================================
|
|
router.put('/:id/status', requireAdmin, async (req, res) => {
|
|
const { id } = req.params;
|
|
const { is_active } = req.body;
|
|
|
|
try {
|
|
await db.run(`UPDATE clinics_devices SET is_active = $1 WHERE id = $2`, [is_active ? 1 : 0, id]);
|
|
res.json({ success: true, message: 'Status atualizado com sucesso' });
|
|
} catch (error) {
|
|
res.status(500).json({ success: false, message: 'Erro interno' });
|
|
}
|
|
});
|
|
|
|
// ==============================================================
|
|
// 4. EXCLUIR CLÍNICA (ADMIN) — apenas o registro do dispositivo
|
|
// ==============================================================
|
|
router.delete('/:id', requireAdmin, async (req, res) => {
|
|
const { id } = req.params;
|
|
try {
|
|
await db.run(`DELETE FROM clinics_devices WHERE id = $1`, [id]);
|
|
res.json({ success: true, message: 'Clínica excluída com sucesso' });
|
|
} catch (error) {
|
|
res.status(500).json({ success: false, message: 'Erro interno' });
|
|
}
|
|
});
|
|
|
|
// ==============================================================
|
|
// 5. WIPE DE DADOS DA CLÍNICA (ADMIN)
|
|
// Apaga: registro do dispositivo + todas as imagens/GTOs do
|
|
// banco + pasta inteira da clínica no bucket Wasabi.
|
|
// CUIDADO: usa client_name como prefixo — só afeta essa clínica.
|
|
// ==============================================================
|
|
router.delete('/:id/wipe', requireAdmin, async (req, res) => {
|
|
const { id } = req.params;
|
|
const log = [];
|
|
|
|
try {
|
|
// 1. Busca o dispositivo para obter o clinic_name (=client_name nas imagens)
|
|
const clinic = await db.get('SELECT * FROM clinics_devices WHERE id = $1', [id]);
|
|
if (!clinic) return res.status(404).json({ success: false, message: 'Clínica não encontrada' });
|
|
|
|
const clinicName = clinic.clinic_name;
|
|
const pcName = clinic.pc_name;
|
|
log.push(`Clínica: "${clinicName}" | PC: "${pcName}"`);
|
|
|
|
// 2. Apaga imagens do banco cujo clinic_name bate com este dispositivo
|
|
// (clinic_name nas imagens é o nome da clínica; client_name é o nome do PC)
|
|
const images = await db.all(
|
|
'SELECT id FROM images WHERE clinic_name = $1',
|
|
[clinicName]
|
|
);
|
|
if (images.length > 0) {
|
|
const ids = images.map(r => r.id);
|
|
await db.run(`DELETE FROM gto_images WHERE image_id = ANY($1::int[])`, [ids]);
|
|
await db.run('DELETE FROM images WHERE clinic_name = $1', [clinicName]);
|
|
log.push(`Banco: ${images.length} imagem(ns) apagada(s)`);
|
|
} else {
|
|
log.push('Banco: nenhuma imagem encontrada para esta clínica');
|
|
}
|
|
|
|
// 3. Apaga pasta inteira no Wasabi
|
|
// Estrutura: sanitize(clinic_name)/sanitize(pc_name)/... → prefixo = clinic_name/
|
|
const wasabiStatus = storage.getWasabiStatus();
|
|
if (wasabiStatus.enabled) {
|
|
const cfg = JSON.parse(
|
|
(await db.get("SELECT value FROM system_config WHERE key='wasabi_config'")).value
|
|
);
|
|
const ep = cfg.wasabiEndpoint.startsWith('http')
|
|
? cfg.wasabiEndpoint : `https://${cfg.wasabiEndpoint}`;
|
|
const s3 = new S3Client({
|
|
region: cfg.wasabiRegion,
|
|
endpoint: ep,
|
|
credentials: { accessKeyId: cfg.wasabiAccessKey, secretAccessKey: cfg.wasabiSecretKey },
|
|
forcePathStyle: true,
|
|
});
|
|
|
|
// sanitize igual ao storage.js: remove chars inválidos, mantém o nome
|
|
const sanitize = (s) => s ? s.trim().replace(/[\/\\?#%*:"<>|]/g, '_') : 'desconhecido';
|
|
const prefix = `${sanitize(clinicName)}/`;
|
|
let totalDeleted = 0;
|
|
let continuationToken;
|
|
|
|
do {
|
|
const listed = await s3.send(new ListObjectsV2Command({
|
|
Bucket: cfg.wasabiBucket,
|
|
Prefix: prefix,
|
|
MaxKeys: 1000,
|
|
ContinuationToken: continuationToken,
|
|
}));
|
|
|
|
const objects = (listed.Contents || []).map(o => ({ Key: o.Key }));
|
|
if (objects.length > 0) {
|
|
await s3.send(new DeleteObjectsCommand({
|
|
Bucket: cfg.wasabiBucket,
|
|
Delete: { Objects: objects, Quiet: true },
|
|
}));
|
|
totalDeleted += objects.length;
|
|
}
|
|
|
|
continuationToken = listed.IsTruncated ? listed.NextContinuationToken : null;
|
|
} while (continuationToken);
|
|
|
|
log.push(`Wasabi: ${totalDeleted} objeto(s) apagado(s) com prefixo "${prefix}"`);
|
|
} else {
|
|
log.push('Wasabi: desativado, pulado');
|
|
}
|
|
|
|
// 4. Apaga o registro do dispositivo
|
|
await db.run('DELETE FROM clinics_devices WHERE id = $1', [id]);
|
|
log.push('Dispositivo removido do sistema');
|
|
|
|
res.json({ success: true, message: 'Dados da clínica apagados com sucesso', log });
|
|
} catch (error) {
|
|
console.error('Erro no wipe da clínica:', error);
|
|
res.status(500).json({ success: false, message: 'Erro interno no wipe', detail: error.message, log });
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|