diff --git a/main.py b/main.py index 277f289..ba0eae5 100644 --- a/main.py +++ b/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') - if c.fetchone()[0] == 0: + count = c.fetchone()[0] + if count == 0: print("Creating default admin user...") admin_hash = get_password_hash(WEB_PASSWORD) c.execute('INSERT INTO users (username, hashed_password, is_admin) VALUES (?, ?, ?)', ('admin', admin_hash, 1)) - - # Associate existing instances with the new admin user - admin_id = c.lastrowid - c.execute('UPDATE instances SET user_id = ? WHERE user_id IS NULL', (admin_id,)) + elif count == 1: + # If there's only one user and it's 'admin', ensure its password matches WEB_PASSWORD + c.execute('SELECT id, username FROM users LIMIT 1') + 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.close() diff --git a/static/app.js b/static/app.js index 81a3ede..2b37740 100644 --- a/static/app.js +++ b/static/app.js @@ -5,10 +5,47 @@ document.addEventListener('DOMContentLoaded', () => { const portInput = document.getElementById('port'); const ufwCmd = document.getElementById('ufw-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 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 = () => { + if (!ufwCmd || !iptablesCmd) return; const port = portInput.value || '4646'; const hostIp = config.firewall_host_ip || window.location.hostname; ufwCmd.textContent = `sudo ufw allow from ${hostIp} to any port ${port} proto tcp`; @@ -18,6 +55,7 @@ document.addEventListener('DOMContentLoaded', () => { const fetchConfig = async () => { try { const res = await fetch('/api/config'); + if (res.status === 401) return showLogin(); if (res.ok) { config = await res.json(); updateFirewallCmds(); @@ -27,18 +65,10 @@ document.addEventListener('DOMContentLoaded', () => { } }; - portInput.addEventListener('input', updateFirewallCmds); - - // Initial fetch of config and instances - fetchConfig(); - updateFirewallCmds(); - const fetchInstances = async () => { try { const res = await fetch('/api/instances'); - if (res.status === 401) { - return; // Let browser handle auth - } + if (res.status === 401) return showLogin(); const data = await res.json(); renderInstances(data); } 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) => { if (instances.length === 0) { instancesList.innerHTML = '
No instances configured yet.
'; @@ -81,6 +124,24 @@ document.addEventListener('DOMContentLoaded', () => { }).join(''); }; + const renderUsers = (users) => { + usersList.innerHTML = users.map(user => ` +
+
+

${escapeHtml(user.username)} ${user.is_admin ? 'Admin' : ''}

+
+
+ + +
+
+ `).join(''); + }; + addForm.addEventListener('submit', async (e) => { e.preventDefault(); const name = document.getElementById('name').value; @@ -91,12 +152,9 @@ document.addEventListener('DOMContentLoaded', () => { try { const res = await fetch('/api/instances', { method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, ip, port, secret }) }); - if (res.ok) { addForm.reset(); document.getElementById('port').value = "4646"; @@ -139,7 +197,7 @@ document.addEventListener('DOMContentLoaded', () => { }); const setupUIForUser = () => { - if (currentUser.is_admin) { + if (currentUser && currentUser.is_admin) { navAdmin.style.display = 'block'; } else { navAdmin.style.display = 'none'; @@ -205,14 +263,43 @@ document.addEventListener('DOMContentLoaded', () => { try { const res = await fetch(`/api/instances/${id}`, { method: 'DELETE' }); if (res.status === 401) return showLogin(); - if (res.ok) { - fetchInstances(); - } + if (res.ok) fetchInstances(); } catch (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) => { const text = document.getElementById(elementId).textContent; 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) { if (!unsafe) return ''; return unsafe.toString() @@ -240,6 +347,10 @@ document.addEventListener('DOMContentLoaded', () => { .replace(/'/g, "'"); } - fetchInstances(); - setInterval(fetchInstances, 5000); + checkAuth(); + setInterval(() => { + if (loginOverlay.style.display === 'none' && document.getElementById('instances-section').classList.contains('active')) { + fetchInstances(); + } + }, 5000); });