1a213f807f
Panel je dosad znao samo dodavati i brisati htpasswd korisnike, a sama auth_basic direktiva se mogla dirati jedino rucno u nginx konfiguraciji. Dodan prekidac po lokaciji — /guacamole/ i /guacadmin/ su odvojeni, pa se zastita moze skinuti sa samo jedne. Stanje se cita iz konfiguracije, ne pamti se zasebno, pa GUI uvijek pokazuje ono sto nginx stvarno radi. Sigurnosne kocnice: - Nakon izmjene ide nginx -t; ako padne, konfiguracija se vraca na staro prije nego se reload uopce dogodi. - Ne moze se ukljuciti zastita ako nema nijednog htpasswd korisnika — to bi zakljucalo sve. - Ne moze se obrisati zadnji korisnik dok je Basic Auth aktivan. Ta rupa je postojala i prije: brisanje zadnjeg korisnika ostavljalo je prazan htpasswd uz aktivan auth_basic, sto zakljuca i Guacamole i sam panel. Parsiranje ide po location bloku s brojanjem viticastih zagrada, ne regexom preko cijele datoteke, da se ne pogodi kriva lokacija. Provjereno na zivoj instalaciji: iskljucenje na /guacamole/ daje 200 bez prijave dok /guacadmin/ ostaje na 401; ponovno ukljucenje vraca 401 i konfiguracija je bajt-identicna originalu. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
172 lines
5.9 KiB
HTML
172 lines
5.9 KiB
HTML
{% extends 'base.html' %}
|
||
{% block title %}Basic Auth – Guacamole Manager{% endblock %}
|
||
|
||
{% block content %}
|
||
<h4 class="mb-4"><i class="bi bi-person-lock me-2"></i>Basic Auth korisnici</h4>
|
||
|
||
<div class="card mb-4">
|
||
<div class="card-header fw-semibold">
|
||
<i class="bi bi-shield-lock me-1"></i> Basic Auth zaštita
|
||
</div>
|
||
<div class="card-body">
|
||
{% for loc, label in auth_locations.items() %}
|
||
<div class="d-flex justify-content-between align-items-center
|
||
{% if not loop.last %}mb-3 pb-3 border-bottom{% endif %}"
|
||
style="border-color:#30363d !important">
|
||
<div>
|
||
<div class="fw-semibold">{{ label }}</div>
|
||
<small class="text-muted"><code>{{ loc }}</code></small>
|
||
</div>
|
||
<div class="form-check form-switch mb-0">
|
||
{% if auth_state[loc] is none %}
|
||
<span class="badge bg-secondary">nije konfiguriran</span>
|
||
{% else %}
|
||
<input class="form-check-input" type="checkbox" role="switch"
|
||
id="sw-{{ loop.index }}" {% if auth_state[loc] %}checked{% endif %}
|
||
onchange="toggleAuth('{{ loc }}', this)">
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
<div class="alert alert-warning mt-3 mb-0 py-2 small">
|
||
<i class="bi bi-exclamation-triangle me-1"></i>
|
||
Isključite li zaštitu, na toj lokaciji nestaje prvi sloj obrane i
|
||
<strong>fail2ban jail <code>nginx-basicauth</code> ostaje bez posla</strong>.
|
||
Za <code>/guacadmin/</code> to znači da je ovaj panel izložen samo
|
||
s vlastitom prijavom.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row g-4">
|
||
<!-- Lista korisnika -->
|
||
<div class="col-md-5">
|
||
<div class="card">
|
||
<div class="card-header d-flex justify-content-between align-items-center">
|
||
<span class="fw-semibold">Korisnici</span>
|
||
<span class="badge bg-secondary">{{ users|length }}</span>
|
||
</div>
|
||
<ul class="list-group list-group-flush" id="user-list">
|
||
{% for user in users %}
|
||
<li class="list-group-item d-flex justify-content-between align-items-center"
|
||
style="background:transparent; border-color:#30363d">
|
||
<span><i class="bi bi-person me-2 text-muted"></i>{{ user }}</span>
|
||
<div class="btn-group btn-group-sm">
|
||
<button class="btn btn-outline-secondary btn-sm"
|
||
onclick="fillUpdate('{{ user }}')">
|
||
<i class="bi bi-key"></i>
|
||
</button>
|
||
<button class="btn btn-outline-danger btn-sm"
|
||
onclick="deleteUser('{{ user }}')">
|
||
<i class="bi bi-trash"></i>
|
||
</button>
|
||
</div>
|
||
</li>
|
||
{% else %}
|
||
<li class="list-group-item text-muted" style="background:transparent">
|
||
Nema korisnika
|
||
</li>
|
||
{% endfor %}
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Forme -->
|
||
<div class="col-md-7">
|
||
<!-- Dodaj korisnika -->
|
||
<div class="card mb-3">
|
||
<div class="card-header fw-semibold">
|
||
<i class="bi bi-person-plus me-1"></i> Dodaj korisnika
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="row g-2">
|
||
<div class="col-5">
|
||
<input id="add-user" type="text" class="form-control form-control-sm"
|
||
placeholder="Korisničko ime">
|
||
</div>
|
||
<div class="col-5">
|
||
<input id="add-pass" type="password" class="form-control form-control-sm"
|
||
placeholder="Lozinka">
|
||
</div>
|
||
<div class="col-2">
|
||
<button class="btn btn-primary btn-sm w-100" onclick="addUser()">
|
||
<i class="bi bi-plus-lg"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Promijeni lozinku -->
|
||
<div class="card">
|
||
<div class="card-header fw-semibold">
|
||
<i class="bi bi-key me-1"></i> Promijeni lozinku
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="row g-2">
|
||
<div class="col-5">
|
||
<input id="upd-user" type="text" class="form-control form-control-sm"
|
||
placeholder="Korisničko ime">
|
||
</div>
|
||
<div class="col-5">
|
||
<input id="upd-pass" type="password" class="form-control form-control-sm"
|
||
placeholder="Nova lozinka">
|
||
</div>
|
||
<div class="col-2">
|
||
<button class="btn btn-outline-primary btn-sm w-100" onclick="updateUser()">
|
||
<i class="bi bi-check-lg"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block scripts %}
|
||
<script>
|
||
function fillUpdate(user) {
|
||
document.getElementById('upd-user').value = user;
|
||
document.getElementById('upd-pass').focus();
|
||
}
|
||
|
||
async function addUser() {
|
||
const username = document.getElementById('add-user').value.trim();
|
||
const password = document.getElementById('add-pass').value;
|
||
const r = await api('/api/basicauth/add', {username, password});
|
||
showToast(r.msg, r.ok);
|
||
if (r.ok) setTimeout(() => location.reload(), 1000);
|
||
}
|
||
|
||
async function updateUser() {
|
||
const username = document.getElementById('upd-user').value.trim();
|
||
const password = document.getElementById('upd-pass').value;
|
||
const r = await api('/api/basicauth/update', {username, password});
|
||
showToast(r.msg, r.ok);
|
||
}
|
||
|
||
async function toggleAuth(location, el) {
|
||
const enabled = el.checked;
|
||
if (!enabled && !confirm(
|
||
`Isključiti Basic Auth za ${location}?\n\n` +
|
||
`Nakon toga se toj lokaciji pristupa bez prompta.`)) {
|
||
el.checked = true;
|
||
return;
|
||
}
|
||
el.disabled = true;
|
||
const r = await api('/api/basicauth/toggle', {location, enabled});
|
||
showToast(r.msg, r.ok);
|
||
if (!r.ok) el.checked = !enabled; // odbijeno — vrati prekidac
|
||
el.disabled = false;
|
||
}
|
||
|
||
async function deleteUser(username) {
|
||
if (!confirm(`Ukloniti korisnika "${username}"?`)) return;
|
||
const r = await api('/api/basicauth/delete', {username});
|
||
showToast(r.msg, r.ok);
|
||
if (r.ok) setTimeout(() => location.reload(), 1000);
|
||
}
|
||
</script>
|
||
{% endblock %}
|