fix: modal usuários, rota wasabi storage-config, sync por permissão — v2.1.2
continuous-integration/webhook Deploy concluido (rx.scoreodonto.com)
continuous-integration/webhook Deploy concluido (rx.scoreodonto.com)
This commit is contained in:
@@ -132,6 +132,16 @@ async function createTables() {
|
||||
)
|
||||
`);
|
||||
console.log('✅ Tabela clinics_devices criada/verificada no PostgreSQL');
|
||||
|
||||
// Tabela de configurações do sistema (key-value)
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS system_config (
|
||||
key VARCHAR(100) PRIMARY KEY,
|
||||
value TEXT,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
console.log('✅ Tabela system_config criada/verificada no PostgreSQL');
|
||||
|
||||
// Verificar colunas faltantes em users (migração)
|
||||
try {
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<div class="logo-icon">
|
||||
<i class="fa-solid fa-tooth"></i>
|
||||
</div>
|
||||
<h2>RF Dental <span style="font-size: 10px; opacity: 0.5; font-weight: 500; margin-left: 4px; vertical-align: middle;">v2.1.1</span></h2>
|
||||
<h2>RF Dental <span style="font-size: 10px; opacity: 0.5; font-weight: 500; margin-left: 4px; vertical-align: middle;">v2.1.2</span></h2>
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-nav">
|
||||
|
||||
@@ -1515,17 +1515,20 @@ function openSyncModal(defaultTab = 'devices') {
|
||||
const modal = document.getElementById('syncModal');
|
||||
if (!modal) return;
|
||||
|
||||
// Usuário não-admin sempre vai para aba upload
|
||||
const isAdmin = document.body.classList.contains('is-admin');
|
||||
const targetTab = (!isAdmin || defaultTab === 'upload') ? 'upload' : 'devices';
|
||||
|
||||
modal.classList.add('active');
|
||||
modal.style.display = 'flex';
|
||||
|
||||
// Resetar abas
|
||||
switchSyncTab(defaultTab);
|
||||
switchSyncTab(targetTab);
|
||||
|
||||
// Limpar formulário de upload
|
||||
resetManualUploadForm();
|
||||
|
||||
// Carregar dados
|
||||
if (defaultTab === 'devices') {
|
||||
if (targetTab === 'devices') {
|
||||
loadSyncDevices();
|
||||
}
|
||||
refreshUploadPatients();
|
||||
@@ -1541,7 +1544,6 @@ function closeSyncModal() {
|
||||
if (!modal) return;
|
||||
|
||||
modal.classList.remove('active');
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
|
||||
function switchSyncTab(tabName) {
|
||||
|
||||
@@ -198,7 +198,7 @@
|
||||
<div class="logo-icon">
|
||||
<i class="fa-solid fa-tooth"></i>
|
||||
</div>
|
||||
<h2>RF Dental <span style="font-size: 10px; opacity: 0.5; font-weight: 500; margin-left: 4px; vertical-align: middle;">v2.1.1</span></h2>
|
||||
<h2>RF Dental <span style="font-size: 10px; opacity: 0.5; font-weight: 500; margin-left: 4px; vertical-align: middle;">v2.1.2</span></h2>
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-nav">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<div class="logo-icon">
|
||||
<i class="fa-solid fa-tooth"></i>
|
||||
</div>
|
||||
<h2>RF Dental <span style="font-size: 10px; opacity: 0.5; font-weight: 500; margin-left: 4px; vertical-align: middle;">v2.1.1</span></h2>
|
||||
<h2>RF Dental <span style="font-size: 10px; opacity: 0.5; font-weight: 500; margin-left: 4px; vertical-align: middle;">v2.1.2</span></h2>
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-nav">
|
||||
@@ -47,7 +47,7 @@
|
||||
<i class="fa-solid fa-users"></i> Gerenciar Usuários
|
||||
</a>
|
||||
<a href="#" class="nav-item" id="menu-sync" onclick="openSyncModal('devices'); return false;">
|
||||
<i class="fa-solid fa-rotate"></i> Sincronização
|
||||
<i class="fa-solid fa-rotate"></i> Dispositivos / Sync
|
||||
</a>
|
||||
<a href="#" class="nav-item" id="menu-plugins" onclick="openPluginsModal(); return false;">
|
||||
<i class="fa-solid fa-plug"></i> Plugins / Wasabi
|
||||
@@ -332,49 +332,49 @@
|
||||
|
||||
<!-- User Management Modal (Admin Only) -->
|
||||
<div id="userManagementModal" class="modal">
|
||||
<div class="modal-content modal-large" style="max-width: 800px; background: var(--bg-surface); border-radius: 12px; box-shadow: var(--shadow-lg); padding: 32px;">
|
||||
<div class="modal-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; border-bottom: 1px solid var(--border-color); padding-bottom: 12px;">
|
||||
<h2 style="font-family: var(--font-heading); color: var(--text-primary); font-size: 1.6rem; font-weight: 600;">👥 Gerenciamento de Usuários</h2>
|
||||
<span class="close-modal" onclick="closeUserManagementModal()" style="font-size: 2rem; cursor: pointer; color: var(--text-secondary);">×</span>
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>👥 Gerenciamento de Usuários</h2>
|
||||
<span class="modal-close" onclick="closeUserManagementModal()">×</span>
|
||||
</div>
|
||||
<div class="modal-body" style="display: flex; gap: 32px; flex-direction: row; flex-wrap: wrap;">
|
||||
<div class="modal-body" style="display: flex; gap: 24px; flex-direction: row; flex-wrap: wrap; align-items: flex-start;">
|
||||
<!-- Formulário de Cadastro (Esquerda) -->
|
||||
<div style="flex: 1; min-width: 280px; background: rgba(0,0,0,0.02); padding: 24px; border-radius: 12px; border: 1px solid var(--border-color);">
|
||||
<h3 style="margin-bottom: 20px; font-family: var(--font-heading); color: var(--text-primary); font-size: 1.25rem;">Cadastrar Novo Usuário</h3>
|
||||
<div style="flex: 1; min-width: 260px; background: rgba(0,0,0,0.02); padding: 20px; border-radius: 10px; border: 1px solid var(--border-color);">
|
||||
<h3 style="margin: 0 0 16px; font-size: 1.05rem; font-weight: 700; color: var(--text-primary);">Cadastrar Novo Usuário</h3>
|
||||
<form id="createUserForm" onsubmit="createUser(event)">
|
||||
<div class="form-group" style="margin-bottom: 16px;">
|
||||
<label style="display:block; margin-bottom:6px; font-weight:600; color:var(--text-primary);">Nome de Usuário *</label>
|
||||
<input type="text" id="new_username" required placeholder="Ex: dentista.ana" style="width: 100%; padding: 12px; border: 1px solid var(--border-color); border-radius: 8px; outline:none; background:var(--bg-color); color:var(--text-primary);">
|
||||
<div class="form-group" style="margin-bottom: 14px;">
|
||||
<label style="display:block; margin-bottom:5px; font-weight:600; font-size:0.85rem; color:var(--text-secondary);">NOME DE USUÁRIO *</label>
|
||||
<input type="text" id="new_username" required placeholder="Ex: dentista.ana" style="width: 100%; padding: 10px 12px; border: 1px solid var(--border-color); border-radius: 8px; outline:none; background:var(--bg-color); color:var(--text-primary); font-size:0.95rem; box-sizing:border-box;">
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom: 16px;">
|
||||
<label style="display:block; margin-bottom:6px; font-weight:600; color:var(--text-primary);">E-mail *</label>
|
||||
<input type="email" id="new_email" required placeholder="ana@consultorio.com" style="width: 100%; padding: 12px; border: 1px solid var(--border-color); border-radius: 8px; outline:none; background:var(--bg-color); color:var(--text-primary);">
|
||||
<div class="form-group" style="margin-bottom: 14px;">
|
||||
<label style="display:block; margin-bottom:5px; font-weight:600; font-size:0.85rem; color:var(--text-secondary);">E-MAIL *</label>
|
||||
<input type="email" id="new_email" required placeholder="ana@consultorio.com" style="width: 100%; padding: 10px 12px; border: 1px solid var(--border-color); border-radius: 8px; outline:none; background:var(--bg-color); color:var(--text-primary); font-size:0.95rem; box-sizing:border-box;">
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom: 16px;">
|
||||
<label style="display:block; margin-bottom:6px; font-weight:600; color:var(--text-primary);">Senha *</label>
|
||||
<input type="password" id="new_password" required placeholder="Senha de acesso" style="width: 100%; padding: 12px; border: 1px solid var(--border-color); border-radius: 8px; outline:none; background:var(--bg-color); color:var(--text-primary);">
|
||||
<div class="form-group" style="margin-bottom: 14px;">
|
||||
<label style="display:block; margin-bottom:5px; font-weight:600; font-size:0.85rem; color:var(--text-secondary);">SENHA *</label>
|
||||
<input type="password" id="new_password" required placeholder="Senha de acesso" style="width: 100%; padding: 10px 12px; border: 1px solid var(--border-color); border-radius: 8px; outline:none; background:var(--bg-color); color:var(--text-primary); font-size:0.95rem; box-sizing:border-box;">
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom: 16px; display: flex; align-items: center; gap: 8px;">
|
||||
<input type="checkbox" id="new_is_admin" style="width: 18px; height: 18px; cursor: pointer; margin: 0;">
|
||||
<label for="new_is_admin" style="font-weight:600; color:var(--text-primary); cursor: pointer; user-select: none; margin: 0;">Permissão de Administrador</label>
|
||||
<div class="form-group" style="margin-bottom: 14px; display: flex; align-items: center; gap: 8px;">
|
||||
<input type="checkbox" id="new_is_admin" style="width: 16px; height: 16px; cursor: pointer; margin: 0; flex-shrink: 0;">
|
||||
<label for="new_is_admin" style="font-weight:600; font-size:0.9rem; color:var(--text-primary); cursor: pointer; user-select: none; margin: 0;">Permissão de Administrador</label>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" style="width: 100%; margin-top: 12px; padding: 12px; border-radius: 8px; font-weight: 600; background:var(--primary-color); color:white; border:none; cursor:pointer;">
|
||||
Cadastrar Usuário
|
||||
<button type="submit" class="btn btn-primary" style="width: 100%; margin-top: 8px; padding: 10px; border-radius: 8px; font-weight: 600; background:var(--primary-color); color:white; border:none; cursor:pointer; font-size:0.95rem;">
|
||||
<i class="fa-solid fa-user-plus"></i> Cadastrar Usuário
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Lista de Usuários (Direita) -->
|
||||
<div style="flex: 1.5; min-width: 320px; display:flex; flex-direction:column;">
|
||||
<h3 style="margin-bottom: 20px; font-family: var(--font-heading); color: var(--text-primary); font-size: 1.25rem;">Usuários Cadastrados</h3>
|
||||
<div style="max-height: 320px; overflow-y: auto; border: 1px solid var(--border-color); border-radius: 12px; background:var(--bg-surface);">
|
||||
<table style="width: 100%; border-collapse: collapse; font-size: 0.95rem; text-align: left;">
|
||||
<div style="flex: 2; min-width: 300px; display:flex; flex-direction:column;">
|
||||
<h3 style="margin: 0 0 16px; font-size: 1.05rem; font-weight: 700; color: var(--text-primary);">Usuários Cadastrados</h3>
|
||||
<div style="flex: 1; overflow-y: auto; border: 1px solid var(--border-color); border-radius: 10px; background:var(--bg-surface);">
|
||||
<table style="width: 100%; border-collapse: collapse; font-size: 0.9rem; text-align: left;">
|
||||
<thead>
|
||||
<tr style="background: rgba(0,0,0,0.03); border-bottom: 1px solid var(--border-color); color:var(--text-primary); font-weight:600;">
|
||||
<th style="padding: 14px 16px;">Usuário</th>
|
||||
<th style="padding: 14px 16px;">E-mail</th>
|
||||
<th style="padding: 14px 16px;">Função</th>
|
||||
<th style="padding: 14px 16px; text-align: center;">Ações</th>
|
||||
<tr style="background: rgba(0,0,0,0.04); border-bottom: 1px solid var(--border-color); color:var(--text-secondary); font-size:0.78rem; text-transform:uppercase; letter-spacing:0.05em; font-weight:700;">
|
||||
<th style="padding: 10px 14px;">Usuário</th>
|
||||
<th style="padding: 10px 14px;">E-mail</th>
|
||||
<th style="padding: 10px 14px;">Função</th>
|
||||
<th style="padding: 10px 14px; text-align: center;">Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="usersListBody">
|
||||
@@ -439,15 +439,15 @@
|
||||
|
||||
<!-- Sync / Upload Modal -->
|
||||
<div id="syncModal" class="modal">
|
||||
<div class="modal-content" style="max-width: 650px; height: auto; border-radius: var(--radius);">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>🔄 Área de Sincronização</h2>
|
||||
<h2>🔄 Sincronização e Envio de Imagens</h2>
|
||||
<span class="modal-close" onclick="closeSyncModal()">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="sync-tabs">
|
||||
<button type="button" id="tabBtnDevices" class="sync-tab-btn active" onclick="switchSyncTab('devices')">💻 Dispositivos Conectados</button>
|
||||
<button type="button" id="tabBtnUpload" class="sync-tab-btn" onclick="switchSyncTab('upload')">📤 Envio Manual</button>
|
||||
<button type="button" id="tabBtnDevices" class="sync-tab-btn admin-only active" onclick="switchSyncTab('devices')">💻 Dispositivos Conectados</button>
|
||||
<button type="button" id="tabBtnUpload" class="sync-tab-btn" onclick="switchSyncTab('upload')">📤 Envio Manual de Imagens</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab 1: Dispositivos -->
|
||||
|
||||
@@ -68,4 +68,96 @@ router.delete('/factory-reset', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ================================================================
|
||||
// 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.' });
|
||||
}
|
||||
});
|
||||
|
||||
// ================================================================
|
||||
// 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 configJson = JSON.stringify({
|
||||
wasabiAccessKey: wasabiAccessKey.trim(),
|
||||
wasabiSecretKey: finalSecret,
|
||||
wasabiBucket: wasabiBucket.trim(),
|
||||
wasabiRegion: (wasabiRegion || 'us-east-1').trim(),
|
||||
wasabiEndpoint: (wasabiEndpoint || 's3.wasabisys.com').trim()
|
||||
});
|
||||
|
||||
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]);
|
||||
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user