diff --git a/public/js/api.js b/public/js/api.js index 5e3cc59..1b00a67 100644 --- a/public/js/api.js +++ b/public/js/api.js @@ -19,6 +19,13 @@ export const api = { authStatus: () => jget('/api/auth/status'), login: (username, password) => jpost('/api/login', { username, password }), logout: () => jpost('/api/logout', {}), + // account management + changePassword: (oldPassword, newPassword) => jpost('/api/account/password', { oldPassword, newPassword }), + listUsers: () => jget('/api/users'), + createUser: (username, password, role) => jpost('/api/users', { username, password, role }), + deleteUser: (username) => jpost('/api/users/delete', { username }), + setUserPassword: (username, password) => jpost('/api/users/password', { username, password }), + setUserRole: (username, role) => jpost('/api/users/role', { username, role }), plugins: () => jget('/api/plugins'), meta: () => jget('/api/meta'), diff --git a/public/js/app.js b/public/js/app.js index a82d633..59556ff 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -29,6 +29,7 @@ const state = { columns: ['name', 'size', 'progress', 'state', 'seeds', 'peers', 'dlspeed', 'upspeed', 'eta', 'ratio', 'category', 'tags', 'addedOn'], searchResults: null, searchQuery: '', + auth: { user: '', role: '' }, }; let viewRenderSeq = 0; @@ -103,11 +104,13 @@ async function loadMeta() { } async function ensureLogin() { - const auth = await api.authStatus(); - if (auth.authenticated) return; - document.getElementById('app').hidden = true; - await new Promise((resolve) => showLogin(auth, resolve)); - document.getElementById('app').hidden = false; + let auth = await api.authStatus(); + if (!auth.authenticated) { + document.getElementById('app').hidden = true; + auth = await new Promise((resolve) => showLogin(auth, resolve)); + document.getElementById('app').hidden = false; + } + state.auth = { user: auth.user || '', role: auth.role || '' }; } function showLogin(auth, done) { @@ -134,9 +137,9 @@ function showLogin(auth, done) { err.hidden = true; form.querySelector('.login-submit').disabled = true; try { - await api.login(host.querySelector('#loginUser').value.trim(), host.querySelector('#loginPass').value); + const res = await api.login(host.querySelector('#loginUser').value.trim(), host.querySelector('#loginPass').value); host.remove(); - done(); + done(res || {}); } catch { err.textContent = 'Invalid username or password'; err.hidden = false; @@ -1438,11 +1441,38 @@ function bindSearchRows() { } /* ===================== Engine/settings view ===================== */ -function renderSettingsView(host) { +async function renderSettingsView(host) { + const isAdmin = state.auth.role === 'admin'; + let users = []; + if (isAdmin) { try { users = await api.listUsers(); } catch (e) { users = []; } } + if (state.view !== 'settings') return; const p = state.meta.preferences; const card = (title, rows) => `
| Username | Role | Created | |
|---|---|---|---|
| ${f.esc(u.username)} | +${f.esc(u.role)} | +${u.createdAt ? f.ago(u.createdAt) : '—'} | ++ + + + |
This is a stubbed engine view — values are read from the mock server. Wire these to a real client's API (qBittorrent WebAPI, Transmission RPC, Deluge JSON-RPC) to make them editable.
${f.esc(username)} will be signed out and must use the new password.
`, + [{ label: 'Cancel', act: closeModal }, { + label: 'Reset', primary: true, act: () => guard(async () => { + const newp = document.getElementById('rpNew').value; + if (!newp) { toast('Password is empty', 'err'); return; } + await api.setUserPassword(username, newp); + closeModal(); + toast('Password reset', 'ok'); + }, 'Failed to reset password'), + }]); + setTimeout(() => document.getElementById('rpNew')?.focus(), 0); } /* ===================== status bar ===================== */