Files
rx.scoreodonto.com/dental-server/routes/admin-clinics.js
T
VPS 4 Deploy Agent 22be2adefd
continuous-integration/webhook Deploy concluído (VPS4)
fix(clinics): adiciona campo senha no formulário de cadastro de clínica
- Form não enviava password → servidor retornava 400 em todo cadastro
- Adicionado input password no modal de cadastro
- pc_name tornado opcional no servidor (já era opcional no UI)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 01:30:52 +02:00

188 lines
7.2 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 {
const clinics = await db.all(`
SELECT id, clinic_name, email, pc_name, machine_token, last_ip, is_active, created_at
FROM clinics_devices
ORDER BY created_at DESC
`);
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 } = 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' });
}
// 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)
VALUES ($1, $2, $3, $4, $5) RETURNING id
`, [clinic_name, email, password_hash, pc_name, machine_token]);
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 clientName = clinic.clinic_name;
log.push(`Clínica: "${clientName}"`);
// 2. Apaga imagens do banco (e GTOs vinculadas via CASCADE)
const images = await db.all('SELECT id FROM images WHERE client_name = $1', [clientName]);
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]);
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)/)
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
);
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
const cleanClient = clientName.trim().replace(/[\/\\?#%*:"<>|]/g, '_');
const prefix = `${cleanClient}/`;
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;