feat(clinics): wipe cirúrgico por dispositivo — apaga banco + pasta Wasabi só da clínica; remove Zerar Sistema da sidebar
continuous-integration/webhook Deploy concluído (VPS4)
continuous-integration/webhook Deploy concluído (VPS4)
- DELETE /admin/clinics/:id/wipe: apaga images/gto_images do banco (WHERE client_name), lista e deleta em batch todos os objetos Wasabi com prefixo 'clinic_name/', e remove o registro do dispositivo - UI: botão 'Limpar dados' (🧹) separado do 🗑️ (remove só o cadastro), modal com aviso, confirmação e log da operação - Sidebar: remove link 'Zerar Sistema'
This commit is contained in:
@@ -3,6 +3,8 @@ 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) => {
|
||||
@@ -82,11 +84,10 @@ router.put('/:id/status', requireAdmin, async (req, res) => {
|
||||
});
|
||||
|
||||
// ==============================================================
|
||||
// 4. EXCLUIR CLÍNICA (ADMIN)
|
||||
// 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' });
|
||||
@@ -95,4 +96,92 @@ router.delete('/:id', requireAdmin, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ==============================================================
|
||||
// 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;
|
||||
|
||||
Reference in New Issue
Block a user