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:
1 parent
36dc12b69d
commit
021da93229
29 files changed
+844
-97
No files matched your search
@@ -7,6 +7,9 @@
|
||||
set -eu
|
||||
|
||||
HOST="${HOST:-{{.Host}}}"
|
||||
# The direct address (not behind a proxy like Cloudflare): the agent turns to
|
||||
# it when the main one is out of reach.
|
||||
FALLBACK_HOST="${FALLBACK_HOST:-{{.Direct}}}"
|
||||
{{if .Secret}}ENROLL_SECRET="${ENROLL_SECRET:-{{.Secret}}}"
|
||||
{{end}}
|
||||
BIN_PATH="/usr/local/bin/lpagent"
|
||||
@@ -108,9 +111,9 @@ fi
|
||||
# --- downloader -------------------------------------------------------------
|
||||
download() { # download URL DEST
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fSL -o "$2" "$1"
|
||||
curl -fSL --connect-timeout 15 -o "$2" "$1"
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
wget -qO "$2" "$1"
|
||||
wget -q -T 30 -O "$2" "$1"
|
||||
else
|
||||
red "Нужен curl или wget."; exit 1
|
||||
fi
|
||||
@@ -118,7 +121,14 @@ download() { # download URL DEST
|
||||
|
||||
bold "Скачивание агента ($ARCH) с $HOST…"
|
||||
TMP="$(mktemp)"
|
||||
download "$HOST/download/agent-linux-$ARCH" "$TMP"
|
||||
if ! download "$HOST/download/agent-linux-$ARCH" "$TMP"; then
|
||||
if [ -n "$FALLBACK_HOST" ] && [ "$FALLBACK_HOST" != "$HOST" ]; then
|
||||
bold "Основной адрес недоступен, скачивание с $FALLBACK_HOST…"
|
||||
download "$FALLBACK_HOST/download/agent-linux-$ARCH" "$TMP"
|
||||
else
|
||||
red "Не удалось скачать агента."; exit 1
|
||||
fi
|
||||
fi
|
||||
install -m 0755 "$TMP" "$BIN_PATH"
|
||||
rm -f "$TMP"
|
||||
green "Бинарник установлен: $BIN_PATH"
|
||||
@@ -129,6 +139,7 @@ chmod 700 "$CONF_DIR"
|
||||
umask 077
|
||||
cat > "$CONF_FILE" <<EOF
|
||||
HOST=$HOST
|
||||
FALLBACK_HOST=$FALLBACK_HOST
|
||||
ENROLL_SECRET=${ENROLL_SECRET:-}
|
||||
AGENT_NAME=$AGENT_NAME
|
||||
STATE_PATH=$STATE_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>';
|
||||
|
||||
@@ -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'; };
|
||||
})();
|
||||
@@ -12,7 +12,7 @@
|
||||
<div class="brand"><a class="home" href="/" title="К списку машин">LPRA</a> · Удалённый доступ</div>
|
||||
<nav>
|
||||
<a href="/">Машины</a>
|
||||
<a href="/settings">Настройки</a>
|
||||
{{if .IsAdmin}}<a href="/settings">Настройки</a>{{end}}
|
||||
<button type="button" class="btn ghost icon acct-btn" id="acct" title="Аккаунт" aria-label="Аккаунт"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/></svg></button>
|
||||
</nav>
|
||||
</header>
|
||||
@@ -47,13 +47,13 @@
|
||||
<a class="btn ghost small icon" href="/agents/{{.ID}}/edit" data-tip="Изменить" aria-label="Изменить">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5z"/></svg>
|
||||
</a>
|
||||
<form method="post" action="/agents/{{.ID}}/delete" style="display:inline"
|
||||
{{if $.IsAdmin}}<form method="post" action="/agents/{{.ID}}/delete" style="display:inline"
|
||||
onsubmit="return confirm('Удалить машину {{.Name}}? Сервис на ней продолжит работать, но перестанет быть виден. Чтобы снова подключиться - переустановите агента.')">
|
||||
<input type="hidden" name="csrf" value="{{$.CSRF}}">
|
||||
<button class="btn danger small icon" type="submit" data-tip="Удалить" aria-label="Удалить">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>
|
||||
</button>
|
||||
</form>
|
||||
</form>{{end}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -61,8 +61,9 @@
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
<p class="muted">Пока нет подключённых машин. Добавьте первую в разделе
|
||||
{{if .IsAdmin}}<p class="muted">Пока нет подключённых машин. Добавьте первую в разделе
|
||||
<a href="/settings">Настройки</a> - там одна команда для установки.</p>
|
||||
{{else}}<p class="muted">Вам пока не выдали ни одной машины - попросите администратора.</p>{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
{{if .IsAdmin}}<div class="card">
|
||||
<h2>Удаление</h2>
|
||||
<p class="muted">Удаляет машину из панели. Сервис на самой машине продолжит работать,
|
||||
но перестанет быть виден; чтобы подключиться снова - переустановите агента.</p>
|
||||
@@ -121,7 +121,7 @@
|
||||
<input type="hidden" name="csrf" value="{{.CSRF}}">
|
||||
<button class="btn danger" type="submit">Удалить машину</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>{{end}}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -8,6 +8,36 @@
|
||||
<script src="{{asset "pin.js"}}"></script>
|
||||
<style>
|
||||
/* An install command, with a button to copy it at its right. */
|
||||
/* Users: role, machines, password sign-in. */
|
||||
.tablewrap { overflow-x:auto; }
|
||||
table.users th { font-size:12px; font-weight:500; color:var(--muted); text-align:left; white-space:nowrap; }
|
||||
table.users td { vertical-align:middle; }
|
||||
table.users select.u-role { width:auto; height:30px; padding:2px 30px 2px 8px; font-size:13px; }
|
||||
table.users .u-agents { vertical-align:middle; margin-left:4px; }
|
||||
table.users .u-count { font-size:12px; margin-left:2px; }
|
||||
table.users .u-lock { width:16px; height:16px; cursor:pointer; }
|
||||
.u-note { font-size:12px; color:var(--ok); opacity:0; }
|
||||
.u-note.bad { color:var(--danger); }
|
||||
.u-note.fade { animation:u-note 3s ease forwards; }
|
||||
@keyframes u-note { 0%, 70% { opacity:1; } 100% { opacity:0; } }
|
||||
.u-add { margin-top:14px; }
|
||||
.u-pick { display:flex; flex-wrap:wrap; gap:6px; align-items:center; animation:u-in .2s ease; }
|
||||
.u-pick[hidden] { display:none; }
|
||||
.u-pick > .muted:first-child { flex-basis:100%; }
|
||||
.u-chip { display:inline-flex; align-items:center; gap:6px; margin:0; padding:5px 10px; border:1px solid var(--border); border-radius:16px;
|
||||
font-size:13px; color:var(--text); cursor:pointer; transition:border-color .15s, background .15s; }
|
||||
.u-chip:has(input:checked) { border-color:var(--accent); background:#12202f; }
|
||||
.u-chip input { width:14px; height:14px; margin:0; }
|
||||
@keyframes u-in { from { opacity:0; transform:translateY(-4px); } }
|
||||
#uadlg { max-width:480px; }
|
||||
.ua-tools { display:flex; gap:8px; margin-bottom:10px; }
|
||||
.ua-tools input { flex:1; height:32px; }
|
||||
.ua-list { max-height:50vh; overflow:auto; border:1px solid var(--border); border-radius:9px; padding:4px; }
|
||||
.ua-item { display:flex; align-items:center; gap:10px; margin:0; padding:8px 10px; border-radius:7px; cursor:pointer; color:var(--text); font-size:14px; }
|
||||
.ua-item:hover { background:#151a21; }
|
||||
.ua-item[hidden] { display:none; }
|
||||
.ua-item input { width:16px; height:16px; margin:0; }
|
||||
.ua-host { margin-left:auto; font-size:12px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
.copybox { position:relative; }
|
||||
.copybox pre { padding-right:48px; }
|
||||
.copybox .copy { position:absolute; top:7px; right:7px; width:30px; height:30px; padding:0;
|
||||
@@ -54,6 +84,12 @@
|
||||
<div class="copybox"><pre id="curl">{{.InstallCurl}}</pre><button type="button" class="btn ghost small icon sq copy" data-copy="curl" title="Скопировать" aria-label="Скопировать"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1"/></svg></button></div>
|
||||
<label>wget</label>
|
||||
<div class="copybox"><pre id="wget">{{.InstallWget}}</pre><button type="button" class="btn ghost small icon sq copy" data-copy="wget" title="Скопировать" aria-label="Скопировать"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1"/></svg></button></div>
|
||||
{{if .Direct}}
|
||||
<label>Короткая через прямой адрес (если основной недоступен)</label>
|
||||
<div class="copybox"><pre id="dshort">{{.DirectShort}}</pre><button type="button" class="btn ghost small icon sq copy" data-copy="dshort" title="Скопировать" aria-label="Скопировать"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1"/></svg></button></div>
|
||||
<label>curl через прямой адрес</label>
|
||||
<div class="copybox"><pre id="dcurl">{{.DirectCurl}}</pre><button type="button" class="btn ghost small icon sq copy" data-copy="dcurl" title="Скопировать" aria-label="Скопировать"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1"/></svg></button></div>
|
||||
{{end}}
|
||||
<p class="muted">Без переменных окружения скрипт спросит секрет интерактивно:
|
||||
<code>curl -fsSL {{.Host}}/install.sh | sudo sh</code></p>
|
||||
</details>
|
||||
@@ -61,11 +97,18 @@
|
||||
|
||||
<div class="card">
|
||||
<h2>Адрес хоста</h2>
|
||||
<p class="muted">URL, на который звонят агенты. Подставляется в скрипт установки.</p>
|
||||
<form method="post" action="/settings/host" class="row">
|
||||
<form method="post" action="/settings/host" class="hostform">
|
||||
<input type="hidden" name="csrf" value="{{.CSRF}}">
|
||||
<input type="text" name="host" value="{{.Host}}" style="max-width:420px">
|
||||
<button type="submit">Сохранить</button>
|
||||
<label for="host">Основной</label>
|
||||
<input type="text" id="host" name="host" value="{{.Host}}" placeholder="https://lpra.example.com">
|
||||
<p class="muted">URL, на который звонят агенты. Подставляется в скрипт установки.</p>
|
||||
<label for="direct">Прямой (без Cloudflare) - необязательно</label>
|
||||
<input type="text" id="direct" name="direct" value="{{.Direct}}" placeholder="https://direct.lpra.example.com">
|
||||
<p class="muted">Имя, которое ведёт прямо на сервер, мимо прокси. Агенты переходят на него, если основной
|
||||
недоступен 30 секунд, и возвращаются, когда он ответит; уже установленные получат его сами. Установщик
|
||||
качает агента с него, если основной не отвечает. Если хост туннеля ниже не задан, туннель тоже идёт на этот хост.
|
||||
Ключи доступа с основного домена работают на прямом, если он его поддомен (direct.lpra.example.com).</p>
|
||||
<div><button type="submit">Сохранить</button></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -103,13 +146,27 @@
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Администраторы</h2>
|
||||
<table>
|
||||
<h2 id="users">Пользователи</h2>
|
||||
<p class="muted" style="font-size:13px">Администратор видит всё. Пользователь - только выданные ему машины, без настроек.
|
||||
«Пароль» - можно ли входить по паролю (выключить можно, когда у человека есть ключ доступа);
|
||||
«Закрепить» - он не сможет сам поменять это в своём аккаунте.</p>
|
||||
<div class="tablewrap"><table class="users">
|
||||
<thead><tr><th>Имя</th><th>Роль</th><th>Вход</th><th title="Вход по паролю">Пароль</th><th title="Не даёт менять вход по паролю самому">Закрепить</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{{range .Admins}}
|
||||
<tr>
|
||||
<td>{{.Username}}</td>
|
||||
<td class="muted">{{with index $.KeyCounts .ID}}ключей доступа: {{.}}{{else}}по паролю{{end}}{{if index $.PassOff .ID}} · пароль выключен{{end}}</td>
|
||||
<tr data-id="{{.ID}}" data-name="{{.Username}}">
|
||||
<td>{{.Username}}{{if eq .ID $.Me.ID}} <span class="muted">(вы)</span>{{end}}</td>
|
||||
<td class="nowrap">
|
||||
<select class="u-role" aria-label="Роль">
|
||||
<option value="admin"{{if .IsAdmin}} selected{{end}}>Администратор</option>
|
||||
<option value="user"{{if not .IsAdmin}} selected{{end}}>Пользователь</option>
|
||||
</select>
|
||||
<button type="button" class="btn ghost small icon sq u-agents" title="Машины пользователя" aria-label="Машины пользователя"{{if .IsAdmin}} hidden{{end}}><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg></button>
|
||||
<span class="muted u-count"{{if .IsAdmin}} hidden{{end}}>{{index $.AgentCounts .ID}}</span>
|
||||
</td>
|
||||
<td class="muted">{{with index $.KeyCounts .ID}}ключей: {{.}}{{else}}только пароль{{end}}</td>
|
||||
<td><input type="checkbox" class="sw u-pw" aria-label="Вход по паролю"{{if not (index $.PassOff .ID)}} checked{{end}}{{if not (index $.KeyCounts .ID)}} disabled title="Нет ключей доступа - пароль выключить нельзя"{{end}}></td>
|
||||
<td><input type="checkbox" class="u-lock" aria-label="Закрепить"{{if .PwLocked}} checked{{end}}></td>
|
||||
<td style="text-align:right" class="nowrap">
|
||||
{{if index $.KeyCounts .ID}}
|
||||
<form method="post" action="/settings/admins/keys/reset" style="display:inline"
|
||||
@@ -120,7 +177,7 @@
|
||||
</form>
|
||||
{{end}}
|
||||
<form method="post" action="/settings/admins/delete" style="display:inline"
|
||||
onsubmit="return confirm('Удалить администратора {{.Username}}?')">
|
||||
onsubmit="return confirm('Удалить {{.Username}}?')">
|
||||
<input type="hidden" name="csrf" value="{{$.CSRF}}">
|
||||
<input type="hidden" name="id" value="{{.ID}}">
|
||||
<button class="btn danger small" type="submit">Удалить</button>
|
||||
@@ -129,17 +186,35 @@
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
<form method="post" action="/settings/admins/add" style="margin-top:14px">
|
||||
</table></div>
|
||||
<form method="post" action="/settings/admins/add" class="u-add">
|
||||
<input type="hidden" name="csrf" value="{{.CSRF}}">
|
||||
<div class="row">
|
||||
<input type="text" name="username" placeholder="логин" style="max-width:200px" required>
|
||||
<input type="password" name="password" placeholder="пароль (мин. 8)" style="max-width:200px" required>
|
||||
<input type="text" name="username" placeholder="логин" style="max-width:200px" required autocomplete="off">
|
||||
<input type="password" name="password" placeholder="пароль (мин. 8)" style="max-width:200px" required autocomplete="new-password">
|
||||
<select name="role" id="newrole" style="max-width:180px">
|
||||
<option value="admin">Администратор</option>
|
||||
<option value="user">Пользователь</option>
|
||||
</select>
|
||||
<button type="submit">Добавить</button>
|
||||
</div>
|
||||
<div class="u-pick" id="newagents" hidden>
|
||||
<div class="muted" style="font-size:13px;margin:10px 0 6px">Машины, к которым у него будет доступ:</div>
|
||||
{{range .AllAgents}}<label class="u-chip"><input type="checkbox" name="agents" value="{{.ID}}"><span>{{.Name}}</span></label>{{else}}<span class="muted">машин пока нет</span>{{end}}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<dialog id="uadlg" tabindex="-1">
|
||||
<div class="dlg-h"><strong style="flex:1">Машины пользователя <span id="uaname"></span></strong>
|
||||
<button type="button" class="btn ghost small icon close x" id="uaclose" title="Закрыть" aria-label="Закрыть"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg></button></div>
|
||||
<div class="dlg-b">
|
||||
<div class="ua-tools"><input type="text" id="uafilter" placeholder="поиск" autocomplete="off"><button type="button" class="btn ghost small" id="uaall">Все</button><button type="button" class="btn ghost small" id="uanone">Ни одной</button></div>
|
||||
<div class="ua-list" id="ualist"></div>
|
||||
<div class="row" style="justify-content:flex-end;margin-top:14px"><button type="button" class="btn ghost" id="uacancel">Отмена</button><button type="button" id="uasave">Сохранить</button></div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<div class="card">
|
||||
<h2>Firewall (ufw)</h2>
|
||||
{{if not .UfwInstalled}}
|
||||
@@ -230,6 +305,7 @@
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<script src="{{asset "users.js"}}"></script>
|
||||
<script src="{{asset "account.js"}}"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user