diff --git a/src/channels/web/static/app.js b/src/channels/web/static/app.js index 6b366482..7892d4e6 100644 --- a/src/channels/web/static/app.js +++ b/src/channels/web/static/app.js @@ -2213,6 +2213,7 @@ function switchTab(tab) { if (tab === 'memory') loadMemoryTree(); if (tab === 'jobs') loadJobs(); if (tab === 'routines') loadRoutines(); + if (tab === 'users') loadUsers(); if (tab === 'logs') applyLogFilters(); if (tab === 'settings') { loadSettingsSubtab(currentSettingsSubtab); @@ -4339,6 +4340,116 @@ function formatRelativeTime(isoString) { return future ? I18n.t('time.daysFromNow', { n: days }) : I18n.t('time.daysAgo', { n: days }); } +// --- Users (admin) --- + +function loadUsers() { + apiFetch('/api/admin/users').then(function(data) { + renderUsersList(data.users || []); + }).catch(function(err) { + // Non-admin users get 403 — show a message instead of an error + var tbody = document.getElementById('users-tbody'); + var empty = document.getElementById('users-empty'); + if (tbody) tbody.innerHTML = ''; + if (empty) { + empty.style.display = 'block'; + empty.textContent = 'Admin access required to manage users.'; + } + }); +} + +function renderUsersList(users) { + var tbody = document.getElementById('users-tbody'); + var empty = document.getElementById('users-empty'); + if (!users || users.length === 0) { + tbody.innerHTML = ''; + empty.style.display = 'block'; + empty.textContent = 'No users found. Create the first user to get started.'; + return; + } + empty.style.display = 'none'; + tbody.innerHTML = users.map(function(u) { + var statusClass = u.status === 'active' ? 'active' : 'failed'; + var roleLabel = u.role === 'admin' ? 'admin' : 'member'; + var actions = ''; + if (u.status === 'active') { + actions += ' '; + } else { + actions += ' '; + } + actions += ''; + return '' + + '' + escapeHtml(u.id.substring(0, 8)) + '…' + + '' + escapeHtml(u.display_name) + '' + + '' + escapeHtml(u.email || '—') + '' + + '' + roleLabel + '' + + '' + escapeHtml(u.status) + '' + + '' + formatRelativeTime(u.created_at) + '' + + '' + actions + '' + + ''; + }).join(''); +} + +function suspendUser(userId) { + apiFetch('/api/admin/users/' + userId + '/suspend', { method: 'POST' }) + .then(function() { loadUsers(); }) + .catch(function(e) { alert('Failed to suspend user: ' + e.message); }); +} + +function activateUser(userId) { + apiFetch('/api/admin/users/' + userId + '/activate', { method: 'POST' }) + .then(function() { loadUsers(); }) + .catch(function(e) { alert('Failed to activate user: ' + e.message); }); +} + +function createTokenForUser(userId, displayName) { + var tokenName = prompt('Token name for ' + displayName + ':', 'api-token'); + if (!tokenName) return; + apiFetch('/api/tokens', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: tokenName, user_id: userId }), + }).then(function(data) { + var banner = document.getElementById('users-token-result'); + banner.style.display = 'block'; + banner.innerHTML = 'Token created! Copy this now — it won\'t be shown again:
' + + '' + escapeHtml(data.token) + '' + + ''; + }).catch(function(e) { alert('Failed to create token: ' + e.message); }); +} + +// Wire up Users tab create form +document.getElementById('users-create-btn')?.addEventListener('click', function() { + document.getElementById('users-create-form').style.display = 'flex'; + document.getElementById('users-token-result').style.display = 'none'; + document.getElementById('user-display-name').focus(); +}); + +document.getElementById('users-create-cancel')?.addEventListener('click', function() { + document.getElementById('users-create-form').style.display = 'none'; +}); + +document.getElementById('users-create-submit')?.addEventListener('click', function() { + var displayName = document.getElementById('user-display-name').value.trim(); + var email = document.getElementById('user-email').value.trim(); + var role = document.getElementById('user-role').value; + if (!displayName) { alert('Display name is required'); return; } + + apiFetch('/api/admin/users', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + display_name: displayName, + email: email || undefined, + role: role, + }), + }).then(function() { + document.getElementById('users-create-form').style.display = 'none'; + document.getElementById('user-display-name').value = ''; + document.getElementById('user-email').value = ''; + loadUsers(); + }).catch(function(e) { alert('Failed to create user: ' + e.message); }); +}); + // --- Gateway status widget --- let gatewayStatusInterval = null; @@ -4933,7 +5044,7 @@ document.addEventListener('keydown', (e) => { // Mod+1-5: switch tabs if (mod && e.key >= '1' && e.key <= '5') { e.preventDefault(); - const tabs = ['chat', 'memory', 'jobs', 'routines', 'settings']; + const tabs = ['chat', 'memory', 'jobs', 'routines', 'users', 'settings']; const idx = parseInt(e.key) - 1; if (tabs[idx]) switchTab(tabs[idx]); return; diff --git a/src/channels/web/static/index.html b/src/channels/web/static/index.html index 7aa2c86f..4192ccc9 100644 --- a/src/channels/web/static/index.html +++ b/src/channels/web/static/index.html @@ -97,6 +97,7 @@ +
@@ -282,6 +283,31 @@ + +
+
+
+

User Management

+ +
+ + + + + + + +
IDDisplay NameEmailRoleStatusCreatedActions
+ +
+
+
diff --git a/src/channels/web/static/style.css b/src/channels/web/static/style.css index 87afea87..6e0dfcf6 100644 --- a/src/channels/web/static/style.css +++ b/src/channels/web/static/style.css @@ -5429,3 +5429,22 @@ body.theme-transition *:not(svg):not(path):not(line):not(circle):not(rect) { --text-muted: #a1a1aa; } } + +/* --- Users Tab --- */ +.users-container { padding: 1rem; } +.users-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem; } +.users-header h3 { margin: 0; font-size: 1.1rem; } +.users-form { display: flex; gap: 0.5rem; align-items: center; margin-bottom: 1rem; flex-wrap: wrap; } +.users-form input, .users-form select { padding: 0.4rem 0.6rem; border-radius: 6px; border: 1px solid var(--border); background: var(--bg-secondary); color: var(--text-primary); font-size: 0.85rem; } +.users-token-banner { background: var(--bg-tertiary); border: 1px solid var(--accent); border-radius: 8px; padding: 0.75rem 1rem; margin-bottom: 1rem; font-size: 0.85rem; } +.token-display { display: inline-block; padding: 0.3rem 0.6rem; background: var(--bg-primary); border-radius: 4px; font-family: var(--font-mono); word-break: break-all; margin: 0.4rem 0; user-select: all; } +.user-id { font-family: var(--font-mono); font-size: 0.8rem; color: var(--text-muted); } +.badge { display: inline-block; padding: 0.15rem 0.5rem; border-radius: 10px; font-size: 0.75rem; background: var(--bg-tertiary); color: var(--text-secondary); } +.badge-admin { background: var(--accent); color: #fff; } +.btn-small { padding: 0.25rem 0.5rem; font-size: 0.75rem; border-radius: 4px; border: 1px solid var(--border); background: var(--bg-secondary); color: var(--text-primary); cursor: pointer; } +.btn-small:hover { background: var(--bg-tertiary); } +.btn-danger { border-color: #ef4444; color: #ef4444; } +.btn-danger:hover { background: #ef4444; color: #fff; } +.btn-primary { background: var(--accent); color: #fff; border: none; padding: 0.4rem 0.8rem; border-radius: 6px; cursor: pointer; font-size: 0.85rem; } +.btn-primary:hover { opacity: 0.9; } +.btn-secondary { background: var(--bg-tertiary); color: var(--text-primary); border: 1px solid var(--border); padding: 0.4rem 0.8rem; border-radius: 6px; cursor: pointer; font-size: 0.85rem; }