From dc1801f4a76c798dd9a546e7720f576fdda8b921 Mon Sep 17 00:00:00 2001 From: VPS 4 Builder Date: Tue, 26 May 2026 20:48:21 +0200 Subject: [PATCH] Ajuste de cadastro/gerenciamento de usuarios e seguranca de endpoints --- dental-server/auth.js | 1 + dental-server/public/admin-clinics.js | 11 +++++++-- dental-server/public/app.js | 26 +++++++++++++++----- dental-server/public/clients.html | 35 +++++++++++++++++++++++++-- dental-server/public/index.html | 11 ++++++++- dental-server/routes/auth.js | 30 +++++++++++++++-------- dental-server/server.js | 14 ++++++++--- 7 files changed, 104 insertions(+), 24 deletions(-) diff --git a/dental-server/auth.js b/dental-server/auth.js index 1b16349..b4e2730 100644 --- a/dental-server/auth.js +++ b/dental-server/auth.js @@ -68,6 +68,7 @@ function generateToken(user, expiresIn = JWT_EXPIRES_IN) { { id: user.id, username: user.username, + email: user.email, is_admin: user.is_admin }, JWT_SECRET, diff --git a/dental-server/public/admin-clinics.js b/dental-server/public/admin-clinics.js index 4c8441d..ba1d897 100644 --- a/dental-server/public/admin-clinics.js +++ b/dental-server/public/admin-clinics.js @@ -44,13 +44,20 @@ document.addEventListener('DOMContentLoaded', () => { try { const response = await fetch(API_URL, fetchOptions); - // Se recebeu 401 ou 403, token expirou — vai para login - if (response.status === 401 || response.status === 403) { + // Se recebeu 401, token expirou ou é inválido — vai para login + if (response.status === 401) { localStorage.removeItem('auth_token'); window.location.href = '/login'; return; } + // Se recebeu 403, acesso proibido (não admin) — volta para a home + if (response.status === 403) { + alert('Acesso negado. Esta área é restrita a administradores.'); + window.location.href = '/'; + return; + } + // Verificar se a resposta é realmente JSON (evita erro com HTML) const contentType = response.headers.get('content-type') || ''; if (!contentType.includes('application/json')) { diff --git a/dental-server/public/app.js b/dental-server/public/app.js index 569680e..8a81160 100644 --- a/dental-server/public/app.js +++ b/dental-server/public/app.js @@ -55,6 +55,8 @@ async function verifyAuth() { if (data.valid) { const isAdmin = !!data.user.is_admin; localStorage.setItem('is_admin', isAdmin ? 'true' : 'false'); + localStorage.setItem('username', data.user.username || ''); + localStorage.setItem('email', data.user.email || ''); // Controle de visibilidade de itens de menu updateAdminMenuVisibility(isAdmin); @@ -1122,9 +1124,10 @@ function openSettingsModal() { const modal = document.getElementById('settingsModal'); if (modal) modal.style.display = 'block'; - // Limpar form + // Limpar form e carregar valores atuais document.getElementById('currentPassword').value = ''; - document.getElementById('newUsername').value = ''; + document.getElementById('newUsername').value = localStorage.getItem('username') || ''; + document.getElementById('newEmail').value = localStorage.getItem('email') || ''; document.getElementById('newPassword').value = ''; } @@ -1138,6 +1141,7 @@ async function updateCredentials(event) { const currentPassword = document.getElementById('currentPassword').value; const newUsername = document.getElementById('newUsername').value.trim(); + const newEmail = document.getElementById('newEmail').value.trim(); const newPassword = document.getElementById('newPassword').value; if (!currentPassword) { @@ -1145,8 +1149,8 @@ async function updateCredentials(event) { return; } - if (!newUsername && !newPassword) { - showToast('Preencha um novo usuário ou nova senha.', 'error'); + if (!newUsername && !newEmail && !newPassword) { + showToast('Preencha um novo usuário, e-mail ou nova senha.', 'error'); return; } @@ -1163,7 +1167,7 @@ async function updateCredentials(event) { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, - body: JSON.stringify({ currentPassword, newUsername, newPassword }) + body: JSON.stringify({ currentPassword, newUsername, newEmail, newPassword }) }); const data = await res.json(); @@ -1178,6 +1182,11 @@ async function updateCredentials(event) { // Atualizar token no localStorage se tiver um novo if (data.token) { localStorage.setItem('auth_token', data.token); + // Salvar novos dados locais decodificando token ou usando retorno + if (data.user) { + localStorage.setItem('username', data.user.username || ''); + localStorage.setItem('email', data.user.email || ''); + } } closeSettingsModal(); @@ -1208,6 +1217,8 @@ function openUserManagementModal() { document.getElementById('new_username').value = ''; document.getElementById('new_email').value = ''; document.getElementById('new_password').value = ''; + const isCheck = document.getElementById('new_is_admin'); + if (isCheck) isCheck.checked = false; loadUsersList(); } @@ -1275,6 +1286,8 @@ async function createUser(event) { const username = document.getElementById('new_username').value.trim(); const email = document.getElementById('new_email').value.trim(); const password = document.getElementById('new_password').value; + const isCheck = document.getElementById('new_is_admin'); + const is_admin = isCheck ? isCheck.checked : false; if (!username || !email || !password) { showToast('Todos os campos obrigatórios.', 'error'); @@ -1294,7 +1307,7 @@ async function createUser(event) { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, - body: JSON.stringify({ username, email, password }) + body: JSON.stringify({ username, email, password, is_admin }) }); const data = await res.json(); @@ -1306,6 +1319,7 @@ async function createUser(event) { document.getElementById('new_username').value = ''; document.getElementById('new_email').value = ''; document.getElementById('new_password').value = ''; + if (isCheck) isCheck.checked = false; // Atualizar lista loadUsersList(); diff --git a/dental-server/public/clients.html b/dental-server/public/clients.html index 12c816c..86e4cc7 100644 --- a/dental-server/public/clients.html +++ b/dental-server/public/clients.html @@ -287,8 +287,25 @@ } async function loadClients() { + const token = localStorage.getItem('auth_token') || ''; try { - const response = await fetch('/api/socket/status'); + const response = await fetch('/api/socket/status', { + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + if (response.status === 401) { + localStorage.removeItem('auth_token'); + window.location.href = '/login'; + return; + } + if (response.status === 403) { + alert('Acesso negado. Esta área é restrita a administradores.'); + window.location.href = '/'; + return; + } + const data = await response.json(); // Atualizar stats @@ -404,6 +421,8 @@ testBtn.disabled = true; testBtn.innerHTML = ' Testando...'; + const token = localStorage.getItem('auth_token') || ''; + try { // Limpar resultados anteriores testResults = {}; @@ -413,10 +432,22 @@ const response = await fetch('/api/socket/test-connection', { method: 'POST', headers: { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` } }); + if (response.status === 401) { + localStorage.removeItem('auth_token'); + window.location.href = '/login'; + return; + } + if (response.status === 403) { + alert('Acesso negado. Esta área é restrita a administradores.'); + window.location.href = '/'; + return; + } + const result = await response.json(); // Processar resultados diff --git a/dental-server/public/index.html b/dental-server/public/index.html index c96672b..ba09666 100644 --- a/dental-server/public/index.html +++ b/dental-server/public/index.html @@ -311,7 +311,12 @@ -
+
+ + +
+ +
@@ -349,6 +354,10 @@
+
+ + +
diff --git a/dental-server/routes/auth.js b/dental-server/routes/auth.js index 871b65d..ec6f06c 100644 --- a/dental-server/routes/auth.js +++ b/dental-server/routes/auth.js @@ -121,7 +121,7 @@ router.post('/create-user', authenticateToken, async (req, res) => { return res.status(403).json({ error: 'Apenas administradores podem criar usuários' }); } - const { username, email, password, clinic_name, pc_name } = req.body; + const { username, email, password, clinic_name, pc_name, is_admin } = req.body; if (!username || !email || !password) { return res.status(400).json({ @@ -146,9 +146,9 @@ router.post('/create-user', authenticateToken, async (req, res) => { // Criar usuário const result = await db.run( - `INSERT INTO users (username, email, password_hash, clinic_name, pc_name, created_at) - VALUES (?, ?, ?, ?, ?, NOW())`, - [username, email, passwordHash, clinic_name || null, pc_name || null] + `INSERT INTO users (username, email, password_hash, clinic_name, pc_name, is_admin, created_at) + VALUES (?, ?, ?, ?, ?, ?, NOW())`, + [username, email, passwordHash, clinic_name || null, pc_name || null, !!is_admin] ); res.json({ @@ -217,14 +217,14 @@ router.post('/users/:id/token', authenticateToken, async (req, res) => { router.put('/update-credentials', authenticateToken, async (req, res) => { try { - const { currentPassword, newUsername, newPassword } = req.body; + const { currentPassword, newUsername, newEmail, newPassword } = req.body; const userId = req.user.id; if (!currentPassword) { return res.status(400).json({ error: 'A senha atual é obrigatória' }); } - if (!newUsername && !newPassword) { + if (!newUsername && !newEmail && !newPassword) { return res.status(400).json({ error: 'Nenhum dado novo foi fornecido para atualização' }); } @@ -241,6 +241,7 @@ router.put('/update-credentials', authenticateToken, async (req, res) => { } let finalUsername = user.username; + let finalEmail = user.email; let finalPasswordHash = user.password_hash; // Verificar e aplicar novo username se fornecido @@ -252,6 +253,15 @@ router.put('/update-credentials', authenticateToken, async (req, res) => { finalUsername = newUsername; } + // Verificar e aplicar novo e-mail se fornecido + if (newEmail && newEmail !== user.email) { + const existing = await db.get('SELECT * FROM users WHERE email = ? AND id != ?', [newEmail, userId]); + if (existing) { + return res.status(409).json({ error: 'Este e-mail já está em uso' }); + } + finalEmail = newEmail; + } + // Verificar e aplicar nova senha se fornecido if (newPassword) { if (newPassword.length < 4) { @@ -262,12 +272,12 @@ router.put('/update-credentials', authenticateToken, async (req, res) => { // Atualizar no banco de dados await db.run( - 'UPDATE users SET username = ?, password_hash = ? WHERE id = ?', - [finalUsername, finalPasswordHash, userId] + 'UPDATE users SET username = ?, email = ?, password_hash = ? WHERE id = ?', + [finalUsername, finalEmail, finalPasswordHash, userId] ); - // Gerar novo token pois o username pode ter mudado - const updatedUser = { id: userId, username: finalUsername, email: user.email }; + // Gerar novo token pois o username ou e-mail podem ter mudado + const updatedUser = { id: userId, username: finalUsername, email: finalEmail, is_admin: user.is_admin }; const newToken = generateToken(updatedUser); res.json({ diff --git a/dental-server/server.js b/dental-server/server.js index 9dea0b3..0a9806c 100644 --- a/dental-server/server.js +++ b/dental-server/server.js @@ -132,8 +132,16 @@ app.post('/api/debug-log', (req, res) => { res.json({ success: true }); }); +// Middleware para verificar se o usuário autenticado é admin +const requireAdmin = (req, res, next) => { + if (req.user && req.user.is_admin) { + return next(); + } + return res.status(403).json({ error: 'Acesso negado. Apenas administradores.' }); +}; + // Status de conexões Socket.IO -app.get('/api/socket/status', (req, res) => { +app.get('/api/socket/status', authenticateToken, requireAdmin, (req, res) => { const sockets = Array.from(io.sockets.sockets.values()); const clients = sockets.map(socket => ({ id: socket.id, @@ -158,7 +166,7 @@ app.get('/api/socket/status', (req, res) => { }); // Endpoint para forçar identificação de um cliente (debug) -app.post('/api/socket/request-identify/:socketId', (req, res) => { +app.post('/api/socket/request-identify/:socketId', authenticateToken, requireAdmin, (req, res) => { const { socketId } = req.params; const socket = io.sockets.sockets.get(socketId); @@ -178,7 +186,7 @@ app.post('/api/socket/request-identify/:socketId', (req, res) => { }); // Endpoint para teste de conexão Socket.IO -app.post('/api/socket/test-connection', async (req, res) => { +app.post('/api/socket/test-connection', authenticateToken, requireAdmin, async (req, res) => { try { const sockets = Array.from(io.sockets.sockets.values()); const totalConnections = sockets.length;