feat: user roles - admins and users limited to the machines given to them, user editor with machines, password sign-in and its lock; direct address without Cloudflare as the agents' fallback and in the installer; version 0.1.2

This commit is contained in:
redtea committed 2026-10-15 10:56:19 +03:00
1 parent 36dc12b69d
commit 021da93229
29 files changed
+844 -97

No files matched your search

+3 -2
View File
@@ -86,8 +86,9 @@
$('.acct-mode').textContent = d.password_off ? 'вход только по ключу доступа' : d.keys.length ? 'вход по паролю или ключу' : 'вход по паролю';
const sw = $('.acct-pw .sw');
sw.checked = !d.password_off;
sw.disabled = !d.keys.length;
$('.acct-pwnote').textContent = !d.keys.length ? 'выключить можно, когда есть хотя бы один ключ'
sw.disabled = !d.keys.length || d.pw_locked;
$('.acct-pwnote').textContent = d.pw_locked ? 'закреплено администратором' + (d.password_off ? ': только по ключу' : '')
: !d.keys.length ? 'выключить можно, когда есть хотя бы один ключ'
: d.password_off ? 'выключен: войти можно только ключом' : 'можно выключить и входить только ключом';
var ul = $('.acct-keys');
ul.innerHTML = d.keys.length ? '' : '<li class="muted">Ключей пока нет.</li>';
+105
View File
@@ -0,0 +1,105 @@
// The users table in Settings: the role, the machines a user may use (a
// dialog with a list to tick), the password sign-in and whether the user
// may switch it themselves. Each change is saved at once.
(function () {
'use strict';
var table = document.querySelector('table.users');
if (!table) return;
var $ = function (id) { return document.getElementById(id); };
function save(id, body) {
return fetch('/api/panel/users/' + id, {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(body)})
.then(function (r) { return r.json(); })
.catch(function () { return {ok: false, error: 'ошибка сети'}; });
}
function note(tr, text, bad) {
var n = tr.querySelector('.u-note');
if (!n) {
n = document.createElement('div');
n.className = 'u-note';
tr.cells[0].appendChild(n);
}
n.textContent = text;
n.classList.toggle('bad', !!bad);
n.classList.remove('fade');
void n.offsetWidth;
n.classList.add('fade');
}
table.addEventListener('change', function (ev) {
var tr = ev.target.closest('tr'), id = tr && tr.dataset.id, el = ev.target;
if (!id) return;
var body = {}, undo;
if (el.classList.contains('u-role')) {
body.role = el.value;
undo = function () { el.value = el.value === 'user' ? 'admin' : 'user'; };
} else if (el.classList.contains('u-pw')) {
body.password_off = !el.checked;
undo = function () { el.checked = !el.checked; };
} else if (el.classList.contains('u-lock')) {
body.pw_locked = el.checked;
undo = function () { el.checked = !el.checked; };
} else {
return;
}
save(id, body).then(function (r) {
if (!r.ok) { undo(); note(tr, r.error || 'не сохранилось', true); return; }
note(tr, 'сохранено');
if (body.role) {
var user = body.role === 'user';
tr.querySelector('.u-agents').hidden = !user;
tr.querySelector('.u-count').hidden = !user;
}
});
});
// ---- the machines of a user ----
var dlg = $('uadlg'), list = $('ualist'), cur = null;
function render(filter) {
var f = (filter || '').toLowerCase();
list.querySelectorAll('label').forEach(function (l) {
l.hidden = f && l.textContent.toLowerCase().indexOf(f) < 0;
});
}
table.addEventListener('click', function (ev) {
var b = ev.target.closest('.u-agents');
if (!b) return;
var tr = b.closest('tr');
cur = tr;
$('uaname').textContent = tr.dataset.name;
list.innerHTML = '<div class="muted">загрузка…</div>';
$('uafilter').value = '';
dlg.showModal();
fetch('/api/panel/users/' + tr.dataset.id + '/agents', {cache: 'no-store'}).then(function (r) { return r.json(); }).then(function (d) {
list.innerHTML = '';
d.agents.forEach(function (a) {
var l = document.createElement('label');
l.className = 'ua-item';
l.innerHTML = '<input type="checkbox"><span class="dot ' + (a.online ? 'on' : 'off') + '"></span><span class="ua-name"></span><span class="muted ua-host"></span>';
l.querySelector('input').value = a.id;
l.querySelector('input').checked = a.on;
l.querySelector('.ua-name').textContent = a.name;
l.querySelector('.ua-host').textContent = a.hostname;
list.appendChild(l);
});
if (!d.agents.length) list.innerHTML = '<div class="muted">машин пока нет</div>';
});
});
$('uafilter').addEventListener('input', function (ev) { render(ev.target.value); });
$('uaall').onclick = function () { list.querySelectorAll('label:not([hidden]) input').forEach(function (i) { i.checked = true; }); };
$('uanone').onclick = function () { list.querySelectorAll('label:not([hidden]) input').forEach(function (i) { i.checked = false; }); };
$('uaclose').onclick = $('uacancel').onclick = function () { dlg.close(); };
$('uasave').onclick = function () {
var ids = [].map.call(list.querySelectorAll('input:checked'), function (i) { return i.value; });
save(cur.dataset.id, {agents: ids}).then(function (r) {
if (!r.ok) { note(cur, r.error || 'не сохранилось', true); return; }
cur.querySelector('.u-count').textContent = ids.length;
note(cur, 'машин: ' + ids.length);
dlg.close();
});
};
// ---- a new user: the machines appear for a plain user ----
var role = $('newrole'), pick = $('newagents');
if (role) role.onchange = function () { pick.hidden = role.value !== 'user'; };
})();