fix(clinics): 3 bugs de unicidade e wipe corrigidos
continuous-integration/webhook Deploy concluído (VPS4)

1. admin-clinics.js — bloqueia cadastro de clinic_name + pc_name duplicados:
   Antes só validava email único. Agora verifica se já existe dispositivo com
   a mesma combinação clinic_name + pc_name (case-insensitive). Sem isso, dois
   PCs com mesmo nome na mesma clínica sobrescreveriam arquivos um do outro
   no Wasabi (caminho idêntico).

2. admin-clinics.js — wipe usava client_name ao invés de clinic_name no banco:
   'DELETE FROM images WHERE client_name = clinic_name' deletava o conjunto
   errado de imagens (client_name é o nome do PC, não da clínica). Corrigido
   para 'WHERE clinic_name = $1'. Wasabi continua usando sanitize(clinic_name)/
   como prefixo (correto).

3. server.js — verificação de duplicata no connect incluía clinicName:
   O check usava só clientName (pc_name), causando falso positivo: dois PCs
   de clínicas diferentes com o mesmo nome (ex: ambos "PC-SALA-1") — o segundo
   era desconectado. Agora verifica clinicName + clientName juntos.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
VPS 4 Deploy Agent
2026-05-31 15:57:37 +02:00
parent 32ac1f119f
commit b6a1d3d68e
2 changed files with 38 additions and 16 deletions
+30 -11
View File
@@ -48,6 +48,21 @@ router.post('/', requireAdmin, async (req, res) => {
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();
@@ -111,25 +126,29 @@ router.delete('/:id/wipe', requireAdmin, async (req, res) => {
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 clientName = clinic.clinic_name;
log.push(`Clínica: "${clientName}"`);
const clinicName = clinic.clinic_name;
const pcName = clinic.pc_name;
log.push(`Clínica: "${clinicName}" | PC: "${pcName}"`);
// 2. Apaga imagens do banco (e GTOs vinculadas via CASCADE)
const images = await db.all('SELECT id FROM images WHERE client_name = $1', [clientName]);
// 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);
// gto_images tem ON DELETE CASCADE, mas garantimos:
await db.run(`DELETE FROM gto_images WHERE image_id = ANY($1::int[])`, [ids]);
await db.run('DELETE FROM images WHERE client_name = $1', [clientName]);
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 (prefixo = sanitize(clinic_name)/)
// 3. Apaga pasta inteira no Wasabi
// Estrutura: sanitize(clinic_name)/sanitize(pc_name)/... → prefixo = clinic_name/
const wasabiStatus = storage.getWasabiStatus();
if (wasabiStatus.enabled) {
// Reutiliza a config do storage carregada em memória via internal accessor
const cfg = JSON.parse(
(await db.get("SELECT value FROM system_config WHERE key='wasabi_config'")).value
);
@@ -142,9 +161,9 @@ router.delete('/:id/wipe', requireAdmin, async (req, res) => {
forcePathStyle: true,
});
// sanitize igual ao storage.js
const cleanClient = clientName.trim().replace(/[\/\\?#%*:"<>|]/g, '_');
const prefix = `${cleanClient}/`;
// 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;
+4 -1
View File
@@ -866,10 +866,13 @@ io.on('connection', (socket) => {
// Se autenticado via machine_token, o pc_name do cadastro é autoritativo
const clientName = (socket.user?.pcName || data.name || 'PC não identificado').trim();
const clientId = data.clientId || '';
const clinicName = socket.user?.clinicName || (data.clinicName || '').trim() || 'Clínica não identificada';
// 1. Validar unicidade em relação aos clientes já conectados
// 1. Validar unicidade: mesmo PC da mesma clínica já conectado (clientId diferente)
// Usa clinicName + clientName para não bloquear PCs homônimos de clínicas diferentes.
const duplicate = connectedClients.find(c =>
c.name.toLowerCase() === clientName.toLowerCase() &&
c.clinicName.toLowerCase() === clinicName.toLowerCase() &&
c.clientId !== clientId
);