feat: implement user management dashboard and visibility access controls
continuous-integration/webhook Deploy concluido (rx.scoreodonto.com)

This commit is contained in:
VPS 4 Builder
2026-05-26 20:45:00 +02:00
parent 9e8d920b75
commit da40ab11f6
4 changed files with 233 additions and 3 deletions
+1 -1
View File
@@ -201,7 +201,7 @@
<button type="submit" class="btn" id="loginBtn">Entrar</button> <button type="submit" class="btn" id="loginBtn">Entrar</button>
<div style="text-align: center; margin-top: 20px; color: var(--text-secondary); font-size: 0.85rem; font-weight: 500;"> <div style="text-align: center; margin-top: 20px; color: var(--text-secondary); font-size: 0.85rem; font-weight: 500;">
v2.0.4 v2.0.5
</div> </div>
</form> </form>
</div> </div>
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "dental-image-server", "name": "dental-image-server",
"version": "2.0.4", "version": "2.0.5",
"description": "Servidor Socket.IO para receber e processar imagens dentais com interface web", "description": "Servidor Socket.IO para receber e processar imagens dentais com interface web",
"main": "server.js", "main": "server.js",
"scripts": { "scripts": {
+175 -1
View File
@@ -52,7 +52,21 @@ async function verifyAuth() {
if (!response.ok) { localStorage.removeItem('auth_token'); window.location.href = '/login'; return false; } if (!response.ok) { localStorage.removeItem('auth_token'); window.location.href = '/login'; return false; }
const data = await response.json(); const data = await response.json();
authVerificationInProgress = false; authVerificationInProgress = false;
if (data.valid) return true; if (data.valid) {
const isAdmin = !!data.user.is_admin;
localStorage.setItem('is_admin', isAdmin ? 'true' : 'false');
// Controle de visibilidade de itens de menu
updateAdminMenuVisibility(isAdmin);
// Se usuário comum tentar acessar clients ou admin-clinics no front, joga de volta pra home
if (!isAdmin && (window.location.pathname === '/clients' || window.location.pathname === '/admin-clinics')) {
window.location.href = '/';
return false;
}
return true;
}
localStorage.removeItem('auth_token'); window.location.href = '/login'; return false; localStorage.removeItem('auth_token'); window.location.href = '/login'; return false;
} catch { } catch {
localStorage.removeItem('auth_token'); localStorage.removeItem('auth_token');
@@ -61,6 +75,18 @@ async function verifyAuth() {
} }
} }
function updateAdminMenuVisibility(isAdmin) {
// Esconder links não autorizados da Sidebar
const connectionsLink = document.querySelector('.sidebar-nav .nav-item[href="/clients"]');
const clinicsLink = document.querySelector('.sidebar-nav .nav-item[href="/admin-clinics"]');
if (connectionsLink) connectionsLink.style.display = isAdmin ? 'flex' : 'none';
if (clinicsLink) clinicsLink.style.display = isAdmin ? 'flex' : 'none';
// Esconder botão de gerenciamento de usuários na toolbar
const btnUserManagement = document.getElementById('btnUserManagement');
if (btnUserManagement) btnUserManagement.style.display = isAdmin ? 'flex' : 'none';
}
async function fetchWithAuth(url, options = {}) { async function fetchWithAuth(url, options = {}) {
const token = getAuthToken(); const token = getAuthToken();
_log.info('FETCH', `${options.method || 'GET'} ${url}`); _log.info('FETCH', `${options.method || 'GET'} ${url}`);
@@ -1170,6 +1196,154 @@ window.openSettingsModal = openSettingsModal;
window.closeSettingsModal = closeSettingsModal; window.closeSettingsModal = closeSettingsModal;
window.updateCredentials = updateCredentials; window.updateCredentials = updateCredentials;
// ================================================================
// GERENCIAMENTO DE USUÁRIOS (ADMIN ONLY)
// ================================================================
function openUserManagementModal() {
const modal = document.getElementById('userManagementModal');
if (modal) modal.style.display = 'block';
// Limpar formulário de criação
document.getElementById('new_username').value = '';
document.getElementById('new_email').value = '';
document.getElementById('new_password').value = '';
loadUsersList();
}
function closeUserManagementModal() {
const modal = document.getElementById('userManagementModal');
if (modal) modal.style.display = 'none';
}
async function loadUsersList() {
const listBody = document.getElementById('usersListBody');
if (!listBody) return;
listBody.innerHTML = '<tr><td colspan="4" style="text-align:center;padding:15px;color:#a0aec0;"><span class="spinner" style="width:14px;height:14px;display:inline-block;"></span> Carregando...</td></tr>';
try {
const token = getAuthToken();
const res = await fetch(`${API_URL}/auth/users`, {
headers: { 'Authorization': `Bearer ${token}` }
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Erro ao carregar usuários.');
if (data.success && data.users) {
listBody.innerHTML = '';
// Decodifica o token atual para não permitir excluir a si mesmo
let currentUserId = null;
try {
const payload = JSON.parse(atob(token.split('.')[1]));
currentUserId = payload.id;
} catch (e) {}
data.users.forEach(u => {
const role = u.is_admin ? '👑 Administrador' : '🩺 Dentista / Comum';
const tr = document.createElement('tr');
tr.style.borderBottom = '1px solid var(--border-color)';
const isSelf = currentUserId && parseInt(u.id) === parseInt(currentUserId);
const deleteBtnHtml = isSelf
? '<span style="color:#a0aec0;font-size:0.85rem;font-style:italic;">Você</span>'
: `<button class="btn btn-danger" onclick="deleteUser(${u.id})" style="padding:4px 8px;font-size:0.8rem;border-radius:4px;cursor:pointer;background:#fee2e2;color:#dc2626;border:1px solid #fecaca;">Excluir</button>`;
tr.innerHTML = `
<td style="padding: 12px 16px; font-weight:600;">${u.username}</td>
<td style="padding: 12px 16px; color:var(--text-secondary);">${u.email}</td>
<td style="padding: 12px 16px; color:var(--text-secondary);">${role}</td>
<td style="padding: 12px 16px; text-align:center;">${deleteBtnHtml}</td>
`;
listBody.appendChild(tr);
});
} else {
listBody.innerHTML = '<tr><td colspan="4" style="text-align:center;color:red;padding:15px;">Falha ao carregar dados.</td></tr>';
}
} catch (e) {
console.error(e);
listBody.innerHTML = `<tr><td colspan="4" style="text-align:center;color:red;padding:15px;">${e.message || 'Erro ao carregar.'}</td></tr>`;
}
}
async function createUser(event) {
event.preventDefault();
const username = document.getElementById('new_username').value.trim();
const email = document.getElementById('new_email').value.trim();
const password = document.getElementById('new_password').value;
if (!username || !email || !password) {
showToast('Todos os campos obrigatórios.', 'error');
return;
}
const submitBtn = event.target.querySelector('button[type="submit"]');
const originalText = submitBtn.innerHTML;
submitBtn.innerHTML = '<span class="spinner" style="width:14px;height:14px;display:inline-block;"></span> Cadastrando...';
submitBtn.disabled = true;
try {
const token = getAuthToken();
const res = await fetch(`${API_URL}/auth/create-user`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ username, email, password })
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Erro ao cadastrar usuário.');
showToast('Usuário cadastrado com sucesso!', 'success');
// Limpar formulário
document.getElementById('new_username').value = '';
document.getElementById('new_email').value = '';
document.getElementById('new_password').value = '';
// Atualizar lista
loadUsersList();
} catch (e) {
console.error(e);
showToast(e.message, 'error');
} finally {
submitBtn.innerHTML = originalText;
submitBtn.disabled = false;
}
}
async function deleteUser(userId) {
if (!confirm('Tem certeza que deseja excluir este usuário permanentemente?')) return;
try {
const token = getAuthToken();
const res = await fetch(`${API_URL}/auth/users/${userId}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` }
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Erro ao excluir usuário.');
showToast('Usuário excluído com sucesso!', 'success');
loadUsersList();
} catch (e) {
console.error(e);
showToast(e.message, 'error');
}
}
window.openUserManagementModal = openUserManagementModal;
window.closeUserManagementModal = closeUserManagementModal;
window.createUser = createUser;
window.deleteUser = deleteUser;
// ================================================================ // ================================================================
// CONFIGURAÇÕES DE PLUGINS (Wasabi Storage) // CONFIGURAÇÕES DE PLUGINS (Wasabi Storage)
// ================================================================ // ================================================================
+56
View File
@@ -92,6 +92,9 @@
<button class="btn btn-primary" onclick="openCreatePatientModal()" style="padding: 10px 16px; border-radius: 10px; font-weight: 600; font-size: 0.95rem; background: var(--primary-color); color: white; border: none; cursor: pointer; display: flex; align-items: center; gap: 6px; box-shadow: 0 4px 10px rgba(79, 70, 229, 0.2);"> <button class="btn btn-primary" onclick="openCreatePatientModal()" style="padding: 10px 16px; border-radius: 10px; font-weight: 600; font-size: 0.95rem; background: var(--primary-color); color: white; border: none; cursor: pointer; display: flex; align-items: center; gap: 6px; box-shadow: 0 4px 10px rgba(79, 70, 229, 0.2);">
Novo Paciente Novo Paciente
</button> </button>
<button id="btnUserManagement" class="btn btn-secondary" onclick="openUserManagementModal()" title="Gerenciar Usuários" style="display: none; padding: 10px 16px; border-radius: 10px; font-weight: 500; font-size: 0.95rem; border: 1px solid var(--border-color); background: var(--bg-surface); color: var(--text-primary); cursor: pointer; align-items: center; gap: 6px;">
👥 Gerenciar Usuários
</button>
<button class="btn btn-secondary" onclick="openSettingsModal()" title="Configurações" style="padding: 10px 16px; border-radius: 10px; font-weight: 500; font-size: 0.95rem; border: 1px solid var(--border-color); background: var(--bg-surface); color: var(--text-primary); cursor: pointer; display: flex; align-items: center; gap: 6px;"> <button class="btn btn-secondary" onclick="openSettingsModal()" title="Configurações" style="padding: 10px 16px; border-radius: 10px; font-weight: 500; font-size: 0.95rem; border: 1px solid var(--border-color); background: var(--bg-surface); color: var(--text-primary); cursor: pointer; display: flex; align-items: center; gap: 6px;">
⚙️ Configurações ⚙️ Configurações
</button> </button>
@@ -322,6 +325,59 @@
</div> </div>
</div> </div>
<!-- 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);">&times;</span>
</div>
<div class="modal-body" style="display: flex; gap: 32px; flex-direction: row; flex-wrap: wrap;">
<!-- 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>
<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>
<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>
<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>
<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>
</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;">
<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>
</thead>
<tbody id="usersListBody">
<!-- Preenchido via JS -->
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Plugins / Wasabi Modal --> <!-- Plugins / Wasabi Modal -->
<div id="pluginsModal" class="modal"> <div id="pluginsModal" class="modal">
<div class="modal-content"> <div class="modal-content">