implement user authentication flow, session management, and admin user management capabilities
This commit is contained in:
parent
7dcda4b5ef
commit
76565477e7
2 changed files with 141 additions and 25 deletions
17
main.py
17
main.py
|
|
@ -80,17 +80,22 @@ def init_db():
|
||||||
)
|
)
|
||||||
''')
|
''')
|
||||||
|
|
||||||
# Create default admin if no users exist
|
# Create or update default admin if no users exist or only one user exists
|
||||||
c.execute('SELECT COUNT(*) FROM users')
|
c.execute('SELECT COUNT(*) FROM users')
|
||||||
if c.fetchone()[0] == 0:
|
count = c.fetchone()[0]
|
||||||
|
if count == 0:
|
||||||
print("Creating default admin user...")
|
print("Creating default admin user...")
|
||||||
admin_hash = get_password_hash(WEB_PASSWORD)
|
admin_hash = get_password_hash(WEB_PASSWORD)
|
||||||
c.execute('INSERT INTO users (username, hashed_password, is_admin) VALUES (?, ?, ?)',
|
c.execute('INSERT INTO users (username, hashed_password, is_admin) VALUES (?, ?, ?)',
|
||||||
('admin', admin_hash, 1))
|
('admin', admin_hash, 1))
|
||||||
|
elif count == 1:
|
||||||
# Associate existing instances with the new admin user
|
# If there's only one user and it's 'admin', ensure its password matches WEB_PASSWORD
|
||||||
admin_id = c.lastrowid
|
c.execute('SELECT id, username FROM users LIMIT 1')
|
||||||
c.execute('UPDATE instances SET user_id = ? WHERE user_id IS NULL', (admin_id,))
|
user = c.fetchone()
|
||||||
|
if user[1] == 'admin':
|
||||||
|
print("Ensuring admin password matches WEB_PASSWORD...")
|
||||||
|
admin_hash = get_password_hash(WEB_PASSWORD)
|
||||||
|
c.execute('UPDATE users SET hashed_password = ? WHERE id = ?', (admin_hash, user[0]))
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
|
||||||
147
static/app.js
147
static/app.js
|
|
@ -5,10 +5,47 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
const portInput = document.getElementById('port');
|
const portInput = document.getElementById('port');
|
||||||
const ufwCmd = document.getElementById('ufw-cmd');
|
const ufwCmd = document.getElementById('ufw-cmd');
|
||||||
const iptablesCmd = document.getElementById('iptables-cmd');
|
const iptablesCmd = document.getElementById('iptables-cmd');
|
||||||
|
const loginOverlay = document.getElementById('login-overlay');
|
||||||
|
const loginForm = document.getElementById('login-form');
|
||||||
|
const loginError = document.getElementById('login-error');
|
||||||
|
const logoutBtn = document.getElementById('logout-btn');
|
||||||
|
const mainNav = document.getElementById('main-nav');
|
||||||
|
const navAdmin = document.getElementById('nav-admin');
|
||||||
|
const usersList = document.getElementById('users-list');
|
||||||
|
const createUserForm = document.getElementById('create-user-form');
|
||||||
|
const changePasswordForm = document.getElementById('change-password-form');
|
||||||
|
const profileSuccess = document.getElementById('profile-success');
|
||||||
|
|
||||||
let config = { firewall_host_ip: null };
|
let config = { firewall_host_ip: null };
|
||||||
|
let currentUser = null;
|
||||||
|
|
||||||
|
const showLogin = () => {
|
||||||
|
loginOverlay.style.display = 'flex';
|
||||||
|
logoutBtn.style.display = 'none';
|
||||||
|
mainNav.style.display = 'none';
|
||||||
|
};
|
||||||
|
|
||||||
|
const hideLogin = () => {
|
||||||
|
loginOverlay.style.display = 'none';
|
||||||
|
logoutBtn.style.display = 'flex';
|
||||||
|
mainNav.style.display = 'flex';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Tab Switching
|
||||||
|
document.querySelectorAll('.nav-item').forEach(item => {
|
||||||
|
item.addEventListener('click', () => {
|
||||||
|
document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active'));
|
||||||
|
document.querySelectorAll('.tab-content').forEach(i => i.classList.remove('active'));
|
||||||
|
item.classList.add('active');
|
||||||
|
document.getElementById(`${item.dataset.tab}-section`).classList.add('active');
|
||||||
|
|
||||||
|
if (item.dataset.tab === 'admin') fetchUsers();
|
||||||
|
if (item.dataset.tab === 'instances') fetchInstances();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const updateFirewallCmds = () => {
|
const updateFirewallCmds = () => {
|
||||||
|
if (!ufwCmd || !iptablesCmd) return;
|
||||||
const port = portInput.value || '4646';
|
const port = portInput.value || '4646';
|
||||||
const hostIp = config.firewall_host_ip || window.location.hostname;
|
const hostIp = config.firewall_host_ip || window.location.hostname;
|
||||||
ufwCmd.textContent = `sudo ufw allow from ${hostIp} to any port ${port} proto tcp`;
|
ufwCmd.textContent = `sudo ufw allow from ${hostIp} to any port ${port} proto tcp`;
|
||||||
|
|
@ -18,6 +55,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
const fetchConfig = async () => {
|
const fetchConfig = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/config');
|
const res = await fetch('/api/config');
|
||||||
|
if (res.status === 401) return showLogin();
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
config = await res.json();
|
config = await res.json();
|
||||||
updateFirewallCmds();
|
updateFirewallCmds();
|
||||||
|
|
@ -27,18 +65,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
portInput.addEventListener('input', updateFirewallCmds);
|
|
||||||
|
|
||||||
// Initial fetch of config and instances
|
|
||||||
fetchConfig();
|
|
||||||
updateFirewallCmds();
|
|
||||||
|
|
||||||
const fetchInstances = async () => {
|
const fetchInstances = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/instances');
|
const res = await fetch('/api/instances');
|
||||||
if (res.status === 401) {
|
if (res.status === 401) return showLogin();
|
||||||
return; // Let browser handle auth
|
|
||||||
}
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
renderInstances(data);
|
renderInstances(data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -46,6 +76,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchUsers = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/users');
|
||||||
|
if (res.status === 401) return showLogin();
|
||||||
|
if (res.ok) {
|
||||||
|
const users = await res.json();
|
||||||
|
renderUsers(users);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to fetch users:', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const renderInstances = (instances) => {
|
const renderInstances = (instances) => {
|
||||||
if (instances.length === 0) {
|
if (instances.length === 0) {
|
||||||
instancesList.innerHTML = '<div class="empty-state">No instances configured yet.</div>';
|
instancesList.innerHTML = '<div class="empty-state">No instances configured yet.</div>';
|
||||||
|
|
@ -81,6 +124,24 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
}).join('');
|
}).join('');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderUsers = (users) => {
|
||||||
|
usersList.innerHTML = users.map(user => `
|
||||||
|
<div class="user-item">
|
||||||
|
<div class="user-info">
|
||||||
|
<h3>${escapeHtml(user.username)} ${user.is_admin ? '<span class="status-badge status-sent">Admin</span>' : ''}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="user-actions">
|
||||||
|
<button class="btn btn-icon" onclick="resetUserPassword(${user.id})" title="Reset Password">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3L15.5 7.5z"></path></svg>
|
||||||
|
</button>
|
||||||
|
<button class="delete-btn" onclick="deleteUser(${user.id})" title="Delete User">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
};
|
||||||
|
|
||||||
addForm.addEventListener('submit', async (e) => {
|
addForm.addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const name = document.getElementById('name').value;
|
const name = document.getElementById('name').value;
|
||||||
|
|
@ -91,12 +152,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/instances', {
|
const res = await fetch('/api/instances', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: { 'Content-Type': 'application/json' },
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ name, ip, port, secret })
|
body: JSON.stringify({ name, ip, port, secret })
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
addForm.reset();
|
addForm.reset();
|
||||||
document.getElementById('port').value = "4646";
|
document.getElementById('port').value = "4646";
|
||||||
|
|
@ -139,7 +197,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const setupUIForUser = () => {
|
const setupUIForUser = () => {
|
||||||
if (currentUser.is_admin) {
|
if (currentUser && currentUser.is_admin) {
|
||||||
navAdmin.style.display = 'block';
|
navAdmin.style.display = 'block';
|
||||||
} else {
|
} else {
|
||||||
navAdmin.style.display = 'none';
|
navAdmin.style.display = 'none';
|
||||||
|
|
@ -205,14 +263,43 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/instances/${id}`, { method: 'DELETE' });
|
const res = await fetch(`/api/instances/${id}`, { method: 'DELETE' });
|
||||||
if (res.status === 401) return showLogin();
|
if (res.status === 401) return showLogin();
|
||||||
if (res.ok) {
|
if (res.ok) fetchInstances();
|
||||||
fetchInstances();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error deleting instance', e);
|
console.error('Error deleting instance', e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
window.deleteUser = async (id) => {
|
||||||
|
if (!confirm('Are you sure you want to delete this user? All their instances will also be deleted.')) return;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/users/${id}`, { method: 'DELETE' });
|
||||||
|
if (res.status === 401) return showLogin();
|
||||||
|
if (res.ok) fetchUsers();
|
||||||
|
else {
|
||||||
|
const data = await res.json();
|
||||||
|
alert(data.detail || 'Failed to delete user');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error deleting user', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.resetUserPassword = async (id) => {
|
||||||
|
const password = prompt('Enter new password for this user:');
|
||||||
|
if (!password) return;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/users/${id}/password`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ password })
|
||||||
|
});
|
||||||
|
if (res.status === 401) return showLogin();
|
||||||
|
if (res.ok) alert('Password updated');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error resetting password', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
window.copyToClipboard = async (elementId) => {
|
window.copyToClipboard = async (elementId) => {
|
||||||
const text = document.getElementById(elementId).textContent;
|
const text = document.getElementById(elementId).textContent;
|
||||||
const btn = event.currentTarget;
|
const btn = event.currentTarget;
|
||||||
|
|
@ -230,6 +317,26 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
portInput.addEventListener('input', updateFirewallCmds);
|
||||||
|
|
||||||
|
const checkAuth = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/auth/status');
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.authenticated) {
|
||||||
|
currentUser = data.user;
|
||||||
|
setupUIForUser();
|
||||||
|
hideLogin();
|
||||||
|
fetchConfig();
|
||||||
|
fetchInstances();
|
||||||
|
} else {
|
||||||
|
showLogin();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showLogin();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
function escapeHtml(unsafe) {
|
function escapeHtml(unsafe) {
|
||||||
if (!unsafe) return '';
|
if (!unsafe) return '';
|
||||||
return unsafe.toString()
|
return unsafe.toString()
|
||||||
|
|
@ -240,6 +347,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
.replace(/'/g, "'");
|
.replace(/'/g, "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkAuth();
|
||||||
|
setInterval(() => {
|
||||||
|
if (loginOverlay.style.display === 'none' && document.getElementById('instances-section').classList.contains('active')) {
|
||||||
fetchInstances();
|
fetchInstances();
|
||||||
setInterval(fetchInstances, 5000);
|
}
|
||||||
|
}, 5000);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue