Feature user and admin panels (#30)

This commit is contained in:
2026-02-16 20:30:36 +00:00
parent c5866ae785
commit 2fc4f6980c
15 changed files with 1805 additions and 111 deletions

273
src/js/admin.js Normal file
View File

@@ -0,0 +1,273 @@
// ==========================================
// Admin Panel
// ==========================================
let _adminListenersInit = false;
function initAdmin() {
if (Backend.user && Backend.user.role === 'admin') {
document.getElementById('adminBtn').style.display = '';
} else {
document.getElementById('adminBtn').style.display = 'none';
}
}
function openAdmin() {
document.getElementById('adminModal').classList.add('active');
initAdminListeners();
switchAdminTab('users');
loadAdminUsers();
}
function closeAdmin() {
document.getElementById('adminModal').classList.remove('active');
}
function initAdminListeners() {
if (_adminListenersInit) return;
_adminListenersInit = true;
document.getElementById('adminShowAddUser').addEventListener('click', () => {
document.getElementById('adminAddUserForm').style.display = '';
document.getElementById('adminShowAddUser').style.display = 'none';
});
document.getElementById('adminCancelAddUser').addEventListener('click', () => {
document.getElementById('adminAddUserForm').style.display = 'none';
document.getElementById('adminShowAddUser').style.display = '';
clearAddUserForm();
});
document.getElementById('adminCreateUserBtn').addEventListener('click', handleCreateUser);
document.getElementById('adminResetCancelBtn').addEventListener('click', closeResetDialog);
document.getElementById('adminResetConfirmBtn').addEventListener('click', handleResetPassword);
document.getElementById('adminResetNewPw').addEventListener('keydown', function(e) {
if (e.key === 'Enter') handleResetPassword();
});
}
function switchAdminTab(tab) {
document.querySelectorAll('.admin-tab').forEach(t => {
t.classList.toggle('active', t.dataset.tab === tab);
});
document.getElementById('adminUsersTab').style.display = tab === 'users' ? '' : 'none';
document.getElementById('adminSettingsTab').style.display = tab === 'settings' ? '' : 'none';
document.getElementById('adminStatsTab').style.display = tab === 'stats' ? '' : 'none';
document.getElementById('adminResetDialog').style.display = 'none';
if (tab === 'settings') loadAdminSettings();
if (tab === 'stats') loadAdminStats();
}
// ── Add User ────────────────────────────────
function clearAddUserForm() {
document.getElementById('adminNewUsername').value = '';
document.getElementById('adminNewEmail').value = '';
document.getElementById('adminNewPassword').value = '';
document.getElementById('adminNewRole').value = 'user';
}
async function handleCreateUser() {
const username = document.getElementById('adminNewUsername').value.trim();
const email = document.getElementById('adminNewEmail').value.trim();
const password = document.getElementById('adminNewPassword').value;
const role = document.getElementById('adminNewRole').value;
if (!username || !email || !password) {
showToast('⚠️ Fill in all fields', 'warning');
return;
}
if (password.length < 8) {
showToast('⚠️ Password must be at least 8 characters', 'warning');
return;
}
try {
await Backend.adminCreateUser(username, email, password, role);
showToast('✅ User "' + username + '" created', 'success');
clearAddUserForm();
document.getElementById('adminAddUserForm').style.display = 'none';
document.getElementById('adminShowAddUser').style.display = '';
loadAdminUsers();
} catch (e) {
showToast('❌ ' + e.message, 'error');
}
}
// ── Users List ──────────────────────────────
async function loadAdminUsers() {
const container = document.getElementById('adminUserList');
container.innerHTML = '<div class="admin-loading">Loading users...</div>';
try {
const data = await Backend.adminListUsers(1, 100);
const users = data.data || [];
if (users.length === 0) {
container.innerHTML = '<div class="admin-empty">No users found</div>';
return;
}
container.innerHTML = users.map(u => {
const isSelf = u.id === Backend.user.id;
const actions = isSelf ? '<span class="admin-you">(you)</span>' : `
<select class="admin-role-select" onchange="changeUserRole('${u.id}', this.value)">
<option value="user" ${u.role === 'user' ? 'selected' : ''}>user</option>
<option value="admin" ${u.role === 'admin' ? 'selected' : ''}>admin</option>
<option value="moderator" ${u.role === 'moderator' ? 'selected' : ''}>moderator</option>
</select>
<button class="btn btn-small btn-secondary" onclick="openResetDialog('${u.id}', '${escapeHtml(u.username)}')" title="Reset password">🔑</button>
<button class="btn btn-small ${u.is_active ? 'btn-warning' : 'btn-success'}" onclick="toggleUserActive('${u.id}', ${!u.is_active})">${u.is_active ? 'Disable' : 'Enable'}</button>
<button class="btn btn-small btn-danger" onclick="deleteUser('${u.id}', '${escapeHtml(u.username)}')">Delete</button>
`;
return `
<div class="admin-user-row" data-id="${u.id}">
<div class="admin-user-info">
<span class="admin-user-name">${escapeHtml(u.username)}</span>
<span class="admin-user-email">${escapeHtml(u.email)}</span>
</div>
<div class="admin-user-meta">
<span class="admin-badge admin-badge-${u.role}">${u.role}</span>
<span class="admin-badge ${u.is_active ? 'admin-badge-active' : 'admin-badge-inactive'}">${u.is_active ? 'active' : 'disabled'}</span>
</div>
<div class="admin-user-actions">${actions}</div>
</div>`;
}).join('');
} catch (e) {
container.innerHTML = '<div class="admin-error">Failed to load users: ' + e.message + '</div>';
}
}
async function changeUserRole(userId, role) {
try {
await Backend.adminUpdateUserRole(userId, role);
showToast('✅ Role updated to ' + role, 'success');
} catch (e) {
showToast('❌ ' + e.message, 'error');
loadAdminUsers();
}
}
async function toggleUserActive(userId, isActive) {
try {
await Backend.adminToggleUserActive(userId, isActive);
showToast('✅ User ' + (isActive ? 'enabled' : 'disabled'), 'success');
loadAdminUsers();
} catch (e) {
showToast('❌ ' + e.message, 'error');
}
}
async function deleteUser(userId, username) {
if (!confirm('Delete user "' + username + '"? This removes all their chats and data.')) return;
try {
await Backend.adminDeleteUser(userId);
showToast('✅ User deleted', 'success');
loadAdminUsers();
} catch (e) {
showToast('❌ ' + e.message, 'error');
}
}
// ── Reset Password ──────────────────────────
let _resetUserId = null;
function openResetDialog(userId, username) {
_resetUserId = userId;
document.getElementById('adminResetTarget').textContent = 'Set new password for: ' + username;
document.getElementById('adminResetNewPw').value = '';
document.getElementById('adminResetDialog').style.display = '';
document.getElementById('adminResetNewPw').focus();
}
function closeResetDialog() {
_resetUserId = null;
document.getElementById('adminResetDialog').style.display = 'none';
}
async function handleResetPassword() {
if (!_resetUserId) return;
const pw = document.getElementById('adminResetNewPw').value;
if (!pw || pw.length < 8) {
showToast('⚠️ Password must be at least 8 characters', 'warning');
return;
}
try {
await Backend.adminResetPassword(_resetUserId, pw);
showToast('✅ Password reset', 'success');
closeResetDialog();
} catch (e) {
showToast('❌ ' + e.message, 'error');
}
}
// ── Settings ────────────────────────────────
async function loadAdminSettings() {
try {
const data = await Backend.adminListSettings();
const settings = data.settings || [];
for (const s of settings) {
if (s.key === 'registration') {
document.getElementById('adminRegToggle').checked = s.value.enabled !== false;
}
if (s.key === 'site') {
document.getElementById('adminSiteName').value = s.value.name || 'Chat Switchboard';
document.getElementById('adminTagline').value = s.value.tagline || 'Multi-Model AI Chat';
}
}
} catch (e) {
showToast('❌ Failed to load settings: ' + e.message, 'error');
}
}
async function saveAdminSettings() {
try {
await Backend.adminUpdateSetting('registration', {
enabled: document.getElementById('adminRegToggle').checked
});
await Backend.adminUpdateSetting('site', {
name: document.getElementById('adminSiteName').value.trim(),
tagline: document.getElementById('adminTagline').value.trim()
});
showToast('✅ Admin settings saved', 'success');
} catch (e) {
showToast('❌ ' + e.message, 'error');
}
}
// ── Stats ───────────────────────────────────
async function loadAdminStats() {
const container = document.getElementById('adminStats');
container.innerHTML = '<div class="admin-loading">Loading stats...</div>';
try {
const stats = await Backend.adminGetStats();
container.innerHTML = '<div class="admin-stats-grid">' +
statCard(stats.total_users, 'Total Users') +
statCard(stats.active_users, 'Active Users') +
statCard(stats.total_chats, 'Chats') +
statCard(stats.total_messages, 'Messages') +
statCard(stats.api_configs, 'API Configs') +
'</div>';
} catch (e) {
container.innerHTML = '<div class="admin-error">Failed to load stats: ' + e.message + '</div>';
}
}
function statCard(value, label) {
return '<div class="admin-stat-card"><div class="admin-stat-value">' +
(value || 0) + '</div><div class="admin-stat-label">' + label + '</div></div>';
}
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}