Files
rx.scoreodonto.com/dental-server/routes/system.js
T
VPS 4 Deploy Agent b59eae1641
continuous-integration/webhook Deploy concluído (VPS4)
fix(wasabi): reload S3 client after saving credentials (403 persistia com chave antiga em memória)
2026-05-30 19:03:30 +02:00

200 lines
6.5 KiB
JavaScript

const express = require('express');
const router = express.Router();
const db = require('../database');
const { verifyPassword } = require('../auth');
const fs = require('fs').promises;
const path = require('path');
const storage = require('../storage');
// ================================================================
// DELETE /api/system/factory-reset - Limpar sistema inteiro
// ================================================================
router.delete('/factory-reset', async (req, res) => {
try {
const { password } = req.body;
const userId = req.user.id;
if (!password) {
return res.status(400).json({ error: 'A senha é obrigatória para confirmar a limpeza.' });
}
// 1. Validar usuário e senha
const user = await db.get('SELECT * FROM users WHERE id = $1', [userId]);
if (!user) {
return res.status(404).json({ error: 'Usuário não encontrado' });
}
const validPassword = await verifyPassword(password, user.password_hash);
if (!validPassword) {
return res.status(401).json({ error: 'A senha atual está incorreta. Acesso negado.' });
}
// 2. Limpar banco de dados (tabelas de imagens e GTOs)
await db.run('DELETE FROM gto_images');
await db.run('DELETE FROM gtos');
await db.run('DELETE FROM images');
// 3. Limpar arquivos físicos (uploads e processed)
const dirsToClean = [
path.join(__dirname, '..', 'uploads'),
path.join(__dirname, '..', 'processed')
];
let deletedFiles = 0;
for (const dirPath of dirsToClean) {
try {
const files = await fs.readdir(dirPath);
for (const file of files) {
// Ignora arquivos como .gitkeep se existirem
if (file === '.gitkeep') continue;
await fs.unlink(path.join(dirPath, file));
deletedFiles++;
}
} catch (err) {
console.error(`Erro ao limpar a pasta ${dirPath}:`, err);
}
}
res.json({
success: true,
message: 'Sistema resetado com sucesso.',
filesDeleted: deletedFiles
});
} catch (error) {
console.error('Erro no Factory Reset:', error);
res.status(500).json({ error: 'Erro interno ao processar a limpeza' });
}
});
// ================================================================
// GET /api/system/storage-config - Ler configurações de armazenamento
// ================================================================
router.get('/storage-config', async (req, res) => {
try {
const row = await db.get(
"SELECT value FROM system_config WHERE key = 'wasabi_config'"
);
if (!row || !row.value) {
// Retorna estrutura vazia — Wasabi não configurado
return res.json({
enabled: false,
wasabiAccessKey: '',
wasabiSecretKey: '',
wasabiBucket: '',
wasabiRegion: '',
wasabiEndpoint: ''
});
}
const config = JSON.parse(row.value);
// Mascara a secret key parcialmente por segurança
const maskedSecret = config.wasabiSecretKey
? config.wasabiSecretKey.substring(0, 4) + '****'
: '';
return res.json({
enabled: !!(config.wasabiAccessKey && config.wasabiBucket),
wasabiAccessKey: config.wasabiAccessKey || '',
wasabiSecretKey: maskedSecret,
wasabiBucket: config.wasabiBucket || '',
wasabiRegion: config.wasabiRegion || '',
wasabiEndpoint: config.wasabiEndpoint || ''
});
} catch (error) {
console.error('Erro ao ler storage-config:', error);
res.status(500).json({ error: 'Erro interno ao ler configurações.' });
}
});
// ================================================================
// GET /api/system/wasabi-status - Retorna status em tempo real
// ================================================================
router.get('/wasabi-status', (req, res) => {
try {
const status = storage.getWasabiStatus();
res.json(status);
} catch (error) {
res.status(500).json({ error: 'Erro ao verificar status do Wasabi.' });
}
});
// ================================================================
// POST /api/system/storage-config - Salvar configurações de armazenamento
// ================================================================
router.post('/storage-config', async (req, res) => {
try {
const {
wasabiAccessKey,
wasabiSecretKey,
wasabiBucket,
wasabiRegion,
wasabiEndpoint
} = req.body;
if (!wasabiAccessKey || !wasabiBucket) {
return res.status(400).json({ error: 'Access Key e Bucket são obrigatórios.' });
}
// Se a secret key vier mascarada (****), preserva a anterior
let finalSecret = wasabiSecretKey;
if (wasabiSecretKey && wasabiSecretKey.includes('****')) {
const existing = await db.get(
"SELECT value FROM system_config WHERE key = 'wasabi_config'"
);
if (existing && existing.value) {
const prev = JSON.parse(existing.value);
finalSecret = prev.wasabiSecretKey || wasabiSecretKey;
}
}
const finalAccessKey = wasabiAccessKey.trim();
const finalBucket = wasabiBucket.trim();
const finalRegion = (wasabiRegion || 'us-east-1').trim();
const finalEndpoint = (wasabiEndpoint || 's3.wasabisys.com').trim();
// 1. Validar as credenciais contra os servidores do Wasabi ANTES de salvar
const validation = await storage.validateCredentials(
finalAccessKey,
finalSecret,
finalBucket,
finalRegion,
finalEndpoint
);
if (!validation.valid) {
return res.status(400).json({ error: validation.message });
}
// 2. Se for válido, salvar no banco
const configJson = JSON.stringify({
wasabiAccessKey: finalAccessKey,
wasabiSecretKey: finalSecret,
wasabiBucket: finalBucket,
wasabiRegion: finalRegion,
wasabiEndpoint: finalEndpoint
});
await db.run(`
INSERT INTO system_config (key, value, updated_at)
VALUES ('wasabi_config', $1, NOW())
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = NOW()
`, [configJson]);
// 3. CRÍTICO: recarregar a config no processo em execução, senão o cliente S3
// em memória continua usando as credenciais antigas (uploads falham com 403).
await storage.loadConfigFromDb();
return res.json({ success: true, message: 'Configurações salvas com sucesso.' });
} catch (error) {
console.error('Erro ao salvar storage-config:', error);
res.status(500).json({ error: 'Erro interno ao salvar configurações.' });
}
});
module.exports = router;