Feature user and admin panels (#30)
This commit is contained in:
273
src/js/admin.js
Normal file
273
src/js/admin.js
Normal 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;
|
||||
}
|
||||
165
src/js/app.js
165
src/js/app.js
@@ -10,10 +10,11 @@ async function init() {
|
||||
Backend.init();
|
||||
|
||||
// Auto-detect backend at same origin
|
||||
let health = null;
|
||||
if (!Backend.baseUrl) {
|
||||
const health = await Backend.checkHealth('');
|
||||
health = await Backend.checkHealth('');
|
||||
if (health && health.database) {
|
||||
Backend.baseUrl = window.location.origin; // same origin
|
||||
Backend.baseUrl = window.location.origin;
|
||||
Backend.save();
|
||||
console.log('🔗 Backend detected at same origin');
|
||||
}
|
||||
@@ -21,27 +22,68 @@ async function init() {
|
||||
|
||||
// If we have saved auth, validate it
|
||||
if (Backend.accessToken) {
|
||||
const health = await Backend.checkHealth(Backend.baseUrl);
|
||||
health = health || await Backend.checkHealth(Backend.baseUrl);
|
||||
if (!health) {
|
||||
console.log('⚠️ Backend unreachable, falling back to unmanaged');
|
||||
Backend.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Decide: splash gate or straight to app
|
||||
if (Backend.baseUrl && !Backend.accessToken) {
|
||||
// Online but not authenticated → show splash, hide app
|
||||
showSplashGate(health);
|
||||
initAuthListeners();
|
||||
return; // Don't init app yet — authSuccess() will do it
|
||||
}
|
||||
|
||||
// Already authed or no backend → go straight to app
|
||||
hideSplashGate();
|
||||
await initApp();
|
||||
}
|
||||
|
||||
async function initApp() {
|
||||
await loadChats();
|
||||
initializeEventListeners();
|
||||
updateQuickModelSelector();
|
||||
renderChatHistory();
|
||||
updateConnectionStatus();
|
||||
|
||||
// If backend available but not logged in, show auth
|
||||
if (Backend.baseUrl && !Backend.accessToken) {
|
||||
showAuthModal();
|
||||
}
|
||||
|
||||
initAdmin();
|
||||
console.log(`✅ Chat Switchboard ready (${Backend.isManaged ? 'managed' : 'unmanaged'} mode)`);
|
||||
}
|
||||
|
||||
function showSplashGate(health) {
|
||||
const splash = document.getElementById('splashGate');
|
||||
const app = document.getElementById('appContainer');
|
||||
splash.classList.add('visible');
|
||||
splash.style.display = 'flex'; // Fallback for cached CSS
|
||||
app.classList.add('app-hidden');
|
||||
app.style.display = 'none'; // Fallback for cached CSS
|
||||
|
||||
// Hide register tab if registration disabled
|
||||
const registerTab = document.getElementById('authTabRegister');
|
||||
if (health && health.registration_enabled === false) {
|
||||
registerTab.style.display = 'none';
|
||||
} else {
|
||||
registerTab.style.display = '';
|
||||
}
|
||||
}
|
||||
|
||||
function hideSplashGate() {
|
||||
const splash = document.getElementById('splashGate');
|
||||
const app = document.getElementById('appContainer');
|
||||
splash.classList.remove('visible');
|
||||
splash.style.display = 'none'; // Hide splash
|
||||
app.classList.remove('app-hidden');
|
||||
app.style.display = ''; // Show app
|
||||
}
|
||||
|
||||
async function authSuccess() {
|
||||
hideSplashGate();
|
||||
await initApp();
|
||||
showToast(`✅ Welcome, ${Backend.user?.username || 'user'}!`, 'success');
|
||||
}
|
||||
|
||||
function initializeEventListeners() {
|
||||
// Settings modal
|
||||
document.getElementById('settingsBtn').addEventListener('click', openSettings);
|
||||
@@ -51,6 +93,12 @@ function initializeEventListeners() {
|
||||
document.getElementById('saveBtn').addEventListener('click', handleSaveSettings);
|
||||
document.getElementById('fetchModelsBtn').addEventListener('click', () => fetchModels(true));
|
||||
|
||||
// Profile
|
||||
document.getElementById('profileChangePwBtn').addEventListener('click', () => {
|
||||
document.getElementById('profileChangePwForm').style.display = '';
|
||||
});
|
||||
document.getElementById('profileSavePwBtn').addEventListener('click', handleChangePassword);
|
||||
|
||||
// Chat controls
|
||||
document.getElementById('newChatBtn').addEventListener('click', newChat);
|
||||
document.getElementById('sendBtn').addEventListener('click', sendMessage);
|
||||
@@ -97,26 +145,21 @@ function initializeEventListeners() {
|
||||
document.getElementById('settingsModal').addEventListener('click', function(e) {
|
||||
if (e.target === this) closeSettings();
|
||||
});
|
||||
document.getElementById('authModal').addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
// Only closeable if user has option to skip (unmanaged available)
|
||||
if (!Backend.baseUrl || Backend.accessToken) {
|
||||
closeAuthModal();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Auth modal
|
||||
document.getElementById('authLoginBtn').addEventListener('click', handleLogin);
|
||||
document.getElementById('authRegisterBtn').addEventListener('click', handleRegister);
|
||||
document.getElementById('authTabLogin').addEventListener('click', () => switchAuthTab('login'));
|
||||
document.getElementById('authTabRegister').addEventListener('click', () => switchAuthTab('register'));
|
||||
document.getElementById('authCloseBtn').addEventListener('click', closeAuthModal);
|
||||
document.getElementById('authSkipBtn').addEventListener('click', skipAuth);
|
||||
|
||||
// Connection status click
|
||||
document.getElementById('connectionStatus').addEventListener('click', handleConnectionClick);
|
||||
|
||||
// Admin panel
|
||||
document.getElementById('adminBtn').addEventListener('click', openAdmin);
|
||||
document.getElementById('adminCloseBtn').addEventListener('click', closeAdmin);
|
||||
document.querySelectorAll('.admin-tab').forEach(tab => {
|
||||
tab.addEventListener('click', () => switchAdminTab(tab.dataset.tab));
|
||||
});
|
||||
document.getElementById('adminSaveSettings').addEventListener('click', saveAdminSettings);
|
||||
document.getElementById('adminModal').addEventListener('click', function(e) {
|
||||
if (e.target === this) closeAdmin();
|
||||
});
|
||||
|
||||
// Global keyboard shortcuts
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.ctrlKey && e.key === ',') {
|
||||
@@ -132,20 +175,6 @@ function initializeEventListeners() {
|
||||
stopGeneration();
|
||||
} else if (document.getElementById('settingsModal').classList.contains('active')) {
|
||||
closeSettings();
|
||||
} else if (document.getElementById('authModal').classList.contains('active')) {
|
||||
closeAuthModal();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Enter key in auth form
|
||||
document.getElementById('authPassword').addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
const activeTab = document.querySelector('.auth-tab.active');
|
||||
if (activeTab && activeTab.id === 'authTabRegister') {
|
||||
// Focus email field first if registering
|
||||
} else {
|
||||
handleLogin();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -176,6 +205,7 @@ function handleSaveSettings() {
|
||||
State.settings.presencePenalty = parseFloat(document.getElementById('presencePenalty').value) || 0;
|
||||
|
||||
saveSettings();
|
||||
saveProfile(); // async, fire-and-forget
|
||||
updateQuickModelSelector();
|
||||
closeSettings();
|
||||
showToast('✅ Settings saved', 'success');
|
||||
@@ -307,18 +337,24 @@ async function regenerateResponse() {
|
||||
|
||||
// ── Auth Flow ───────────────────────────────
|
||||
|
||||
function showAuthModal() {
|
||||
document.getElementById('authModal').classList.add('active');
|
||||
document.getElementById('authLogin').value = '';
|
||||
document.getElementById('authPassword').value = '';
|
||||
document.getElementById('authUsername').value = '';
|
||||
document.getElementById('authEmail').value = '';
|
||||
document.getElementById('authError').textContent = '';
|
||||
switchAuthTab('login');
|
||||
}
|
||||
let _authListenersInit = false;
|
||||
function initAuthListeners() {
|
||||
if (_authListenersInit) return;
|
||||
_authListenersInit = true;
|
||||
|
||||
function closeAuthModal() {
|
||||
document.getElementById('authModal').classList.remove('active');
|
||||
document.getElementById('authLoginBtn').addEventListener('click', handleLogin);
|
||||
document.getElementById('authRegisterBtn').addEventListener('click', handleRegister);
|
||||
document.getElementById('authTabLogin').addEventListener('click', () => switchAuthTab('login'));
|
||||
document.getElementById('authTabRegister').addEventListener('click', () => switchAuthTab('register'));
|
||||
document.getElementById('authSkipBtn').addEventListener('click', skipAuth);
|
||||
|
||||
// Enter key in auth forms
|
||||
document.getElementById('authPassword').addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter') handleLogin();
|
||||
});
|
||||
document.getElementById('authRegPassword').addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter') handleRegister();
|
||||
});
|
||||
}
|
||||
|
||||
function switchAuthTab(tab) {
|
||||
@@ -343,11 +379,7 @@ async function handleLogin() {
|
||||
setAuthLoading(true);
|
||||
try {
|
||||
await Backend.login(login, password);
|
||||
closeAuthModal();
|
||||
await loadChats();
|
||||
renderChatHistory();
|
||||
updateConnectionStatus();
|
||||
showToast(`✅ Welcome back, ${Backend.user.username}`, 'success');
|
||||
await authSuccess();
|
||||
} catch (e) {
|
||||
document.getElementById('authError').textContent = e.message;
|
||||
} finally {
|
||||
@@ -372,11 +404,7 @@ async function handleRegister() {
|
||||
setAuthLoading(true);
|
||||
try {
|
||||
await Backend.register(username, email, password);
|
||||
closeAuthModal();
|
||||
await loadChats();
|
||||
renderChatHistory();
|
||||
updateConnectionStatus();
|
||||
showToast(`✅ Welcome, ${Backend.user.username}!`, 'success');
|
||||
await authSuccess();
|
||||
} catch (e) {
|
||||
document.getElementById('authError').textContent = e.message;
|
||||
} finally {
|
||||
@@ -393,10 +421,10 @@ function setAuthLoading(loading) {
|
||||
});
|
||||
}
|
||||
|
||||
function skipAuth() {
|
||||
closeAuthModal();
|
||||
async function skipAuth() {
|
||||
Backend.clear();
|
||||
updateConnectionStatus();
|
||||
hideSplashGate();
|
||||
await initApp();
|
||||
showToast('📴 Running in offline mode', 'success');
|
||||
}
|
||||
|
||||
@@ -406,7 +434,7 @@ function updateConnectionStatus() {
|
||||
const el = document.getElementById('connectionStatus');
|
||||
if (Backend.isManaged) {
|
||||
el.className = 'connection-status managed';
|
||||
el.innerHTML = `<span class="status-dot"></span> ${Backend.user?.username || 'Connected'}`;
|
||||
el.innerHTML = `<span class="status-dot"></span> ${Backend.user?.display_name || Backend.user?.username || 'Connected'}`;
|
||||
el.title = 'Managed mode – click to sign out';
|
||||
} else if (Backend.baseUrl) {
|
||||
el.className = 'connection-status available';
|
||||
@@ -425,14 +453,13 @@ async function handleConnectionClick() {
|
||||
await Backend.logout();
|
||||
State.chats = [];
|
||||
State.currentChatId = null;
|
||||
loadChats();
|
||||
newChat();
|
||||
renderChatHistory();
|
||||
updateConnectionStatus();
|
||||
showSplashGate(null);
|
||||
initAuthListeners();
|
||||
showToast('👋 Signed out', 'success');
|
||||
}
|
||||
} else if (Backend.baseUrl) {
|
||||
showAuthModal();
|
||||
showSplashGate(null);
|
||||
initAuthListeners();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -201,6 +201,99 @@ const Backend = {
|
||||
});
|
||||
},
|
||||
|
||||
// ── Profile & Settings ──────────────────
|
||||
|
||||
async getProfile() {
|
||||
return this._authedFetch('/api/v1/profile');
|
||||
},
|
||||
|
||||
async updateProfile(updates) {
|
||||
return this._authedFetch('/api/v1/profile', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(updates)
|
||||
});
|
||||
},
|
||||
|
||||
async changePassword(currentPassword, newPassword) {
|
||||
return this._authedFetch('/api/v1/profile/password', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
current_password: currentPassword,
|
||||
new_password: newPassword
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
async getSettings() {
|
||||
return this._authedFetch('/api/v1/settings');
|
||||
},
|
||||
|
||||
async updateSettings(settings) {
|
||||
return this._authedFetch('/api/v1/settings', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(settings)
|
||||
});
|
||||
},
|
||||
|
||||
// ── Admin ───────────────────────────────
|
||||
|
||||
async adminListUsers(page = 1, perPage = 50) {
|
||||
return this._authedFetch(`/api/v1/admin/users?page=${page}&per_page=${perPage}`);
|
||||
},
|
||||
|
||||
async adminCreateUser(username, email, password, role) {
|
||||
return this._authedFetch('/api/v1/admin/users', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ username, email, password, role })
|
||||
});
|
||||
},
|
||||
|
||||
async adminResetPassword(userId, newPassword) {
|
||||
return this._authedFetch(`/api/v1/admin/users/${userId}/reset-password`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ new_password: newPassword })
|
||||
});
|
||||
},
|
||||
|
||||
async adminUpdateUserRole(userId, role) {
|
||||
return this._authedFetch(`/api/v1/admin/users/${userId}/role`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ role })
|
||||
});
|
||||
},
|
||||
|
||||
async adminToggleUserActive(userId, isActive) {
|
||||
return this._authedFetch(`/api/v1/admin/users/${userId}/active`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ is_active: isActive })
|
||||
});
|
||||
},
|
||||
|
||||
async adminDeleteUser(userId) {
|
||||
return this._authedFetch(`/api/v1/admin/users/${userId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
},
|
||||
|
||||
async adminListSettings() {
|
||||
return this._authedFetch('/api/v1/admin/settings');
|
||||
},
|
||||
|
||||
async adminGetSetting(key) {
|
||||
return this._authedFetch(`/api/v1/admin/settings/${key}`);
|
||||
},
|
||||
|
||||
async adminUpdateSetting(key, value) {
|
||||
return this._authedFetch(`/api/v1/admin/settings/${key}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(value)
|
||||
});
|
||||
},
|
||||
|
||||
async adminGetStats() {
|
||||
return this._authedFetch('/api/v1/admin/stats');
|
||||
},
|
||||
|
||||
// ── Models ──────────────────────────────
|
||||
|
||||
async listAllModels() {
|
||||
|
||||
79
src/js/ui.js
79
src/js/ui.js
@@ -88,9 +88,88 @@ function updateQuickModelSelector() {
|
||||
|
||||
function openSettings() {
|
||||
updateSettingsUI();
|
||||
updateProfileSection();
|
||||
document.getElementById('settingsModal').classList.add('active');
|
||||
}
|
||||
|
||||
function updateProfileSection() {
|
||||
const profileSection = document.getElementById('profileSection');
|
||||
const unmanagedSettings = document.getElementById('unmanagedSettings');
|
||||
|
||||
if (Backend.isManaged) {
|
||||
profileSection.style.display = '';
|
||||
unmanagedSettings.style.display = 'none';
|
||||
loadProfileIntoSettings();
|
||||
} else {
|
||||
profileSection.style.display = 'none';
|
||||
unmanagedSettings.style.display = '';
|
||||
}
|
||||
// Always reset password form
|
||||
document.getElementById('profileChangePwForm').style.display = 'none';
|
||||
}
|
||||
|
||||
async function loadProfileIntoSettings() {
|
||||
try {
|
||||
const profile = await Backend.getProfile();
|
||||
document.getElementById('profileDisplayName').value = profile.display_name || '';
|
||||
document.getElementById('profileEmail').value = profile.email || '';
|
||||
} catch (e) {
|
||||
console.warn('Failed to load profile:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveProfile() {
|
||||
if (!Backend.isManaged) return;
|
||||
|
||||
const displayName = document.getElementById('profileDisplayName').value.trim();
|
||||
const email = document.getElementById('profileEmail').value.trim();
|
||||
|
||||
const updates = {};
|
||||
if (displayName !== (Backend.user.display_name || '')) {
|
||||
updates.display_name = displayName || null;
|
||||
}
|
||||
if (email && email !== Backend.user.email) {
|
||||
updates.email = email;
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length === 0) return;
|
||||
|
||||
try {
|
||||
const profile = await Backend.updateProfile(updates);
|
||||
// Update local user state
|
||||
if (profile.display_name !== undefined) Backend.user.display_name = profile.display_name;
|
||||
if (profile.email) Backend.user.email = profile.email;
|
||||
Backend.save();
|
||||
showToast('✅ Profile updated', 'success');
|
||||
} catch (e) {
|
||||
showToast('❌ ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleChangePassword() {
|
||||
const current = document.getElementById('profileCurrentPw').value;
|
||||
const newPw = document.getElementById('profileNewPw').value;
|
||||
|
||||
if (!current || !newPw) {
|
||||
showToast('⚠️ Fill in both fields', 'warning');
|
||||
return;
|
||||
}
|
||||
if (newPw.length < 8) {
|
||||
showToast('⚠️ New password must be at least 8 characters', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await Backend.changePassword(current, newPw);
|
||||
showToast('✅ Password updated', 'success');
|
||||
document.getElementById('profileCurrentPw').value = '';
|
||||
document.getElementById('profileNewPw').value = '';
|
||||
document.getElementById('profileChangePwForm').style.display = 'none';
|
||||
} catch (e) {
|
||||
showToast('❌ ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function closeSettings() {
|
||||
document.getElementById('settingsModal').classList.remove('active');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user