Ajuste de cadastro/gerenciamento de usuarios e seguranca de endpoints
continuous-integration/webhook Falha no deploy (rx.scoreodonto.com)

This commit is contained in:
VPS 4 Builder
2026-05-26 20:48:21 +02:00
parent da40ab11f6
commit dc1801f4a7
7 changed files with 104 additions and 24 deletions
+1
View File
@@ -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,
+9 -2
View File
@@ -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')) {
+20 -6
View File
@@ -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();
+33 -2
View File
@@ -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 = '<span class="loading"></span> 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
+10 -1
View File
@@ -311,7 +311,12 @@
<input type="text" id="newUsername" class="form-control" placeholder="Deixe em branco para não alterar">
</div>
<div class="form-group">
<div class="form-group" style="margin-top: 15px;">
<label for="newEmail">Novo E-mail de Acesso</label>
<input type="email" id="newEmail" class="form-control" placeholder="Deixe em branco para não alterar">
</div>
<div class="form-group" style="margin-top: 15px;">
<label for="newPassword">Nova Senha</label>
<input type="password" id="newPassword" class="form-control" placeholder="Deixe em branco para não alterar">
</div>
@@ -349,6 +354,10 @@
<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>
<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>
<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>
+20 -10
View File
@@ -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({
+11 -3
View File
@@ -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;