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' }); } // 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) 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 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;