Initial commit — GuacPanel v1.0
All-in-one installer za Apache Guacamole s Nginx reverse proxy, fail2ban, SSL (Let's Encrypt / Self-signed), GeoIP blokiranjem i Flask web GUI za upravljanje. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Backup{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h4 class="mb-0"><i class="bi bi-archive me-2"></i>Backup</h4>
|
||||
<button class="btn btn-primary" onclick="createBackup(this)">
|
||||
<i class="bi bi-plus-circle"></i> Napravi backup
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info small mb-4">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
Backup uključuje: Guacamole konfiguraciju, PostgreSQL bazu, nginx konfig,
|
||||
Basic Auth korisnike, fail2ban pravila i SSL certifikate.
|
||||
Sprema se u <code>/root/</code> (čitljivo samo root-u).
|
||||
</div>
|
||||
|
||||
<!-- Lista backupa -->
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span class="fw-semibold">Dostupni backupi</span>
|
||||
<span class="badge bg-secondary">{{ backups|length }}</span>
|
||||
</div>
|
||||
{% if backups %}
|
||||
<div class="list-group list-group-flush">
|
||||
{% for b in backups %}
|
||||
<div class="list-group-item d-flex justify-content-between align-items-center"
|
||||
style="background:transparent; border-color:#30363d">
|
||||
<div>
|
||||
<i class="bi bi-file-zip me-2 text-muted"></i>
|
||||
<span class="font-monospace small">{{ b }}</span>
|
||||
</div>
|
||||
<button class="btn btn-outline-danger btn-sm" onclick="deleteBackup('{{ b }}')">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card-body text-muted">
|
||||
<i class="bi bi-inbox me-1"></i> Nema dostupnih backupa
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Progress -->
|
||||
<div id="backup-progress" class="card mt-4 d-none">
|
||||
<div class="card-body d-flex align-items-center gap-3">
|
||||
<div class="spinner-border spinner-border-sm text-primary"></div>
|
||||
<span>Kreiranje backupa, molim pričekajte...</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
async function createBackup(btn) {
|
||||
const orig = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Backup...';
|
||||
document.getElementById('backup-progress').classList.remove('d-none');
|
||||
|
||||
const r = await api('/api/backup/create');
|
||||
showToast(r.msg, r.ok);
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = orig;
|
||||
document.getElementById('backup-progress').classList.add('d-none');
|
||||
if (r.ok) setTimeout(() => location.reload(), 1500);
|
||||
}
|
||||
|
||||
async function deleteBackup(name) {
|
||||
if (!confirm(`Obrisati backup "${name}"?`)) return;
|
||||
const r = await api('/api/backup/delete', {name});
|
||||
showToast(r.msg, r.ok);
|
||||
if (r.ok) setTimeout(() => location.reload(), 800);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,147 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="hr" data-bs-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}Guacamole Manager{% endblock %}</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css">
|
||||
<style>
|
||||
body { min-height: 100vh; background: #0f1117; }
|
||||
#sidebar {
|
||||
width: 230px; min-height: 100vh; background: #161b22;
|
||||
border-right: 1px solid #30363d; position: fixed; top: 0; left: 0;
|
||||
display: flex; flex-direction: column; z-index: 100;
|
||||
}
|
||||
#sidebar .brand {
|
||||
padding: 1.25rem 1rem; border-bottom: 1px solid #30363d;
|
||||
font-weight: 700; font-size: 1rem; color: #58a6ff;
|
||||
display: flex; align-items: center; gap: .5rem;
|
||||
}
|
||||
#sidebar .nav-link {
|
||||
color: #8b949e; padding: .55rem 1rem; border-radius: 6px;
|
||||
margin: 2px 8px; display: flex; align-items: center; gap: .6rem;
|
||||
font-size: .9rem; transition: all .15s;
|
||||
}
|
||||
#sidebar .nav-link:hover { background: #21262d; color: #c9d1d9; }
|
||||
#sidebar .nav-link.active { background: #1f6feb22; color: #58a6ff; }
|
||||
#sidebar .nav-link i { font-size: 1rem; width: 18px; text-align: center; }
|
||||
#sidebar .sidebar-footer {
|
||||
margin-top: auto; padding: 1rem; border-top: 1px solid #30363d; font-size: .8rem;
|
||||
}
|
||||
#content { margin-left: 230px; padding: 2rem; }
|
||||
.status-dot {
|
||||
width: 10px; height: 10px; border-radius: 50%; display: inline-block;
|
||||
}
|
||||
.status-ok { background: #3fb950; box-shadow: 0 0 6px #3fb95066; }
|
||||
.status-err { background: #f85149; box-shadow: 0 0 6px #f8514966; }
|
||||
.status-warn { background: #d29922; box-shadow: 0 0 6px #d2992266; }
|
||||
.card { background: #161b22; border: 1px solid #30363d; }
|
||||
.card-header { background: #1c2128; border-bottom: 1px solid #30363d; }
|
||||
.badge-country {
|
||||
background: #1f6feb33; color: #58a6ff; border: 1px solid #1f6feb66;
|
||||
font-size: .75rem; padding: 3px 8px; border-radius: 12px; margin: 2px;
|
||||
display: inline-block;
|
||||
}
|
||||
#log-output {
|
||||
background: #010409; color: #c9d1d9; font-family: monospace;
|
||||
font-size: .8rem; height: 400px; overflow-y: auto; padding: 1rem;
|
||||
border-radius: 6px; border: 1px solid #30363d;
|
||||
}
|
||||
.toast-container { position: fixed; bottom: 1.5rem; right: 1.5rem; z-index: 9999; }
|
||||
@media (max-width: 768px) {
|
||||
#sidebar { width: 100%; min-height: auto; position: relative; }
|
||||
#content { margin-left: 0; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div id="sidebar">
|
||||
<div class="brand">
|
||||
<i class="bi bi-display"></i> Guacamole
|
||||
</div>
|
||||
<nav class="nav flex-column mt-2">
|
||||
<a class="nav-link {% if request.endpoint == 'dashboard' %}active{% endif %}"
|
||||
href="{{ url_for('dashboard') }}">
|
||||
<i class="bi bi-grid-1x2"></i> Dashboard
|
||||
</a>
|
||||
<a class="nav-link {% if request.endpoint == 'basicauth' %}active{% endif %}"
|
||||
href="{{ url_for('basicauth') }}">
|
||||
<i class="bi bi-person-lock"></i> Basic Auth
|
||||
</a>
|
||||
<a class="nav-link {% if request.endpoint == 'geoip' %}active{% endif %}"
|
||||
href="{{ url_for('geoip') }}">
|
||||
<i class="bi bi-globe"></i> GeoIP
|
||||
</a>
|
||||
<a class="nav-link {% if request.endpoint == 'fail2ban' %}active{% endif %}"
|
||||
href="{{ url_for('fail2ban') }}">
|
||||
<i class="bi bi-shield-x"></i> fail2ban
|
||||
</a>
|
||||
<a class="nav-link {% if request.endpoint == 'guacamole' %}active{% endif %}"
|
||||
href="{{ url_for('guacamole') }}">
|
||||
<i class="bi bi-boxes"></i> Kontejneri
|
||||
</a>
|
||||
<a class="nav-link {% if request.endpoint == 'ssl' %}active{% endif %}"
|
||||
href="{{ url_for('ssl') }}">
|
||||
<i class="bi bi-lock"></i> SSL
|
||||
</a>
|
||||
<a class="nav-link {% if request.endpoint == 'updates' %}active{% endif %}"
|
||||
href="{{ url_for('updates') }}">
|
||||
<i class="bi bi-arrow-up-circle"></i> Ažuriranja
|
||||
</a>
|
||||
<a class="nav-link {% if request.endpoint == 'backup' %}active{% endif %}"
|
||||
href="{{ url_for('backup') }}">
|
||||
<i class="bi bi-archive"></i> Backup
|
||||
</a>
|
||||
<a class="nav-link {% if request.endpoint == 'settings' %}active{% endif %}"
|
||||
href="{{ url_for('settings') }}">
|
||||
<i class="bi bi-gear"></i> Postavke
|
||||
</a>
|
||||
</nav>
|
||||
<div class="sidebar-footer text-muted">
|
||||
<a href="{{ url_for('logout') }}" class="text-danger text-decoration-none">
|
||||
<i class="bi bi-box-arrow-right"></i> Odjava
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div id="content">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<!-- Toast container -->
|
||||
<div class="toast-container">
|
||||
<div id="toast" class="toast align-items-center border-0" role="alert">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body" id="toast-body"></div>
|
||||
<button type="button" class="btn-close me-2 m-auto" data-bs-dismiss="toast"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
const toast = new bootstrap.Toast(document.getElementById('toast'), {delay: 4000});
|
||||
|
||||
function showToast(msg, ok=true) {
|
||||
const el = document.getElementById('toast');
|
||||
el.className = `toast align-items-center border-0 text-bg-${ok ? 'success' : 'danger'}`;
|
||||
document.getElementById('toast-body').textContent = msg;
|
||||
toast.show();
|
||||
}
|
||||
|
||||
async function api(url, body={}) {
|
||||
const r = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
return r.json();
|
||||
}
|
||||
</script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,122 @@
|
||||
{% 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="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 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 %}
|
||||
@@ -0,0 +1,130 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Dashboard – Guacamole Manager{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h4 class="mb-0">Dashboard</h4>
|
||||
<small class="text-muted">{{ domain }}</small>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="loadStatus()">
|
||||
<i class="bi bi-arrow-clockwise"></i> Osvježi
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Kontejneri -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-12"><h6 class="text-muted text-uppercase small mb-2">Docker kontejneri</h6></div>
|
||||
{% for name, label in [('guacamole','Guacamole Web'), ('guacd','Guacd Daemon'), ('guac_postgres','PostgreSQL')] %}
|
||||
<div class="col-md-4">
|
||||
<div class="card p-3" id="card-{{ name }}">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="status-dot status-warn" id="dot-{{ name }}"></span>
|
||||
<div>
|
||||
<div class="fw-semibold">{{ label }}</div>
|
||||
<small class="text-muted" id="txt-{{ name }}">Učitavanje...</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Servisi -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-12"><h6 class="text-muted text-uppercase small mb-2">Sistemski servisi</h6></div>
|
||||
{% for name, label, icon in [('nginx','Nginx','bi-server'), ('fail2ban','fail2ban','bi-shield'), ('docker','Docker','bi-box')] %}
|
||||
<div class="col-md-4">
|
||||
<div class="card p-3">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="status-dot status-warn" id="dot-svc-{{ name }}"></span>
|
||||
<div>
|
||||
<div class="fw-semibold"><i class="bi {{ icon }} me-1"></i>{{ label }}</div>
|
||||
<small class="text-muted" id="txt-svc-{{ name }}">Učitavanje...</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Info kartice -->
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<div class="card p-3">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<i class="bi bi-globe fs-3 text-info"></i>
|
||||
<div>
|
||||
<div class="small text-muted">GeoIP blokiranje</div>
|
||||
<span id="badge-geoip" class="badge bg-secondary">Provjera...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card p-3">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<i class="bi bi-lock fs-3 text-success"></i>
|
||||
<div>
|
||||
<div class="small text-muted">SSL certifikat</div>
|
||||
<span id="badge-ssl" class="badge bg-secondary">Provjera...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card p-3">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<i class="bi bi-shield-x fs-3 text-danger"></i>
|
||||
<div>
|
||||
<div class="small text-muted">Zabanjeni IP-ovi</div>
|
||||
<span id="badge-banned" class="fs-4 fw-bold">...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
async function loadStatus() {
|
||||
const data = await fetch('/api/status').then(r => r.json());
|
||||
|
||||
// Containers
|
||||
const containers = {
|
||||
guacamole: data.containers.guacamole,
|
||||
guacd: data.containers.guacd,
|
||||
guac_postgres: data.containers.guac_postgres
|
||||
};
|
||||
for (const [name, running] of Object.entries(containers)) {
|
||||
document.getElementById(`dot-${name}`).className =
|
||||
`status-dot ${running ? 'status-ok' : 'status-err'}`;
|
||||
document.getElementById(`txt-${name}`).textContent =
|
||||
running ? 'Aktivan' : 'Nije aktivan';
|
||||
}
|
||||
|
||||
// Services
|
||||
for (const [name, active] of Object.entries(data.services)) {
|
||||
document.getElementById(`dot-svc-${name}`).className =
|
||||
`status-dot ${active ? 'status-ok' : 'status-err'}`;
|
||||
document.getElementById(`txt-svc-${name}`).textContent =
|
||||
active ? 'Aktivan' : 'Nije aktivan';
|
||||
}
|
||||
|
||||
// Badges
|
||||
const geoip = document.getElementById('badge-geoip');
|
||||
geoip.textContent = data.geoip ? 'Aktivno' : 'Neaktivno';
|
||||
geoip.className = `badge ${data.geoip ? 'bg-success' : 'bg-secondary'}`;
|
||||
|
||||
const ssl = document.getElementById('badge-ssl');
|
||||
ssl.textContent = data.ssl ? 'Aktivan' : 'Nije aktivan';
|
||||
ssl.className = `badge ${data.ssl ? 'bg-success' : 'bg-warning text-dark'}`;
|
||||
|
||||
document.getElementById('badge-banned').textContent = data.banned;
|
||||
}
|
||||
|
||||
loadStatus();
|
||||
setInterval(loadStatus, 15000);
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,124 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}fail2ban – Guacamole Manager{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h4 class="mb-4"><i class="bi bi-shield-x me-2"></i>fail2ban</h4>
|
||||
|
||||
<div class="row g-4">
|
||||
|
||||
<!-- Zabanjeni IP-ovi -->
|
||||
<div class="col-md-8">
|
||||
{% for jail, ips in banned.items() %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header d-flex justify-content-between">
|
||||
<span class="fw-semibold">{{ jail }}</span>
|
||||
<span class="badge {{ 'bg-danger' if ips else 'bg-secondary' }}">
|
||||
{{ ips|length }} zabanjeno
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{% if ips %}
|
||||
<table class="table table-sm mb-0" style="border-color:#30363d">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>IP adresa</th>
|
||||
<th class="text-end">Akcija</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for ip in ips %}
|
||||
<tr>
|
||||
<td class="font-monospace">{{ ip }}</td>
|
||||
<td class="text-end">
|
||||
<button class="btn btn-outline-success btn-sm"
|
||||
onclick="unban('{{ ip }}', '{{ jail }}')">
|
||||
<i class="bi bi-check-circle"></i> Odbani
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="text-muted small p-3 mb-0">Nema zabanjenih IP-ova</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Akcije i postavke -->
|
||||
<div class="col-md-4">
|
||||
<!-- Ručni ban -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-header fw-semibold">
|
||||
<i class="bi bi-shield-plus me-1"></i> Ručni ban
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-2">
|
||||
<input id="ban-ip" type="text" class="form-control form-control-sm"
|
||||
placeholder="192.168.1.100">
|
||||
</div>
|
||||
<select id="ban-jail" class="form-select form-select-sm mb-2">
|
||||
{% for j in banned.keys() %}
|
||||
<option>{{ j }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button class="btn btn-danger btn-sm w-100" onclick="banIP()">
|
||||
<i class="bi bi-ban"></i> Baniraj IP
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Postavke -->
|
||||
<div class="card">
|
||||
<div class="card-header fw-semibold">
|
||||
<i class="bi bi-sliders me-1"></i> Postavke
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-2">
|
||||
<label class="form-label small text-muted">Max pokušaja (maxretry)</label>
|
||||
<input id="maxretry" type="number" class="form-control form-control-sm"
|
||||
value="{{ cfg.get('maxretry', 5) }}" min="1" max="20">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small text-muted">Trajanje bana (sekunde)</label>
|
||||
<input id="bantime" type="number" class="form-control form-control-sm"
|
||||
value="{{ cfg.get('bantime', 3600) }}" min="60">
|
||||
<small class="text-muted">3600=1h · 86400=1dan · 604800=1tjedan</small>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-sm w-100" onclick="saveSettings()">
|
||||
<i class="bi bi-floppy"></i> Spremi
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
async function unban(ip, jail) {
|
||||
const r = await api('/api/fail2ban/unban', {ip, jail});
|
||||
showToast(r.msg, r.ok);
|
||||
if (r.ok) setTimeout(() => location.reload(), 1000);
|
||||
}
|
||||
|
||||
async function banIP() {
|
||||
const ip = document.getElementById('ban-ip').value.trim();
|
||||
const jail = document.getElementById('ban-jail').value;
|
||||
if (!ip) return showToast('Unesite IP adresu', false);
|
||||
const r = await api('/api/fail2ban/ban', {ip, jail});
|
||||
showToast(r.msg, r.ok);
|
||||
if (r.ok) setTimeout(() => location.reload(), 1000);
|
||||
}
|
||||
|
||||
async function saveSettings() {
|
||||
const r = await api('/api/fail2ban/settings', {
|
||||
maxretry: document.getElementById('maxretry').value,
|
||||
bantime: document.getElementById('bantime').value
|
||||
});
|
||||
showToast(r.msg, r.ok);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,159 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}GeoIP – Guacamole Manager{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h4 class="mb-0"><i class="bi bi-globe me-2"></i>GeoIP blokiranje</h4>
|
||||
{% if enabled %}
|
||||
<span class="badge bg-success fs-6">Aktivno</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary fs-6">Neaktivno</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if enabled %}
|
||||
<!-- GeoIP je aktivan -->
|
||||
<div class="row g-4">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header fw-semibold">Dozvoljene države</div>
|
||||
<div class="card-body">
|
||||
<div id="country-badges" class="mb-3">
|
||||
{% for cc in countries.split() %}
|
||||
<span class="badge-country">{{ cc }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="input-group input-group-sm">
|
||||
<input id="countries-input" type="text" class="form-control"
|
||||
placeholder="HR BA SI RS AT" value="{{ countries }}">
|
||||
<button class="btn btn-primary" onclick="updateCountries()">
|
||||
<i class="bi bi-check-lg"></i> Spremi
|
||||
</button>
|
||||
</div>
|
||||
<small class="text-muted">ISO-2 kodovi zemalja, razdvojeni razmakom</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header fw-semibold">Akcije</div>
|
||||
<div class="card-body d-flex flex-column gap-2">
|
||||
<button class="btn btn-outline-info" onclick="refreshDB()">
|
||||
<i class="bi bi-cloud-download"></i> Ažuriraj GeoIP bazu
|
||||
</button>
|
||||
<button class="btn btn-outline-danger" onclick="disableGeoIP()">
|
||||
<i class="bi bi-x-circle"></i> Onemogući GeoIP
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<!-- GeoIP nije aktivan -->
|
||||
<div class="card">
|
||||
<div class="card-header fw-semibold">
|
||||
<i class="bi bi-globe me-1"></i> Konfiguracija GeoIP blokiranja
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-info small mb-4">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
Potreban je besplatni MaxMind račun:
|
||||
<a href="https://www.maxmind.com/en/geolite2/signup" target="_blank" class="alert-link">
|
||||
maxmind.com/en/geolite2/signup
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small text-muted">MaxMind Account ID</label>
|
||||
<input id="mm-id" type="text" class="form-control" value="{{ mm_id }}"
|
||||
placeholder="123456">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small text-muted">MaxMind License Key</label>
|
||||
<input id="mm-key" type="password" class="form-control"
|
||||
placeholder="••••••••••••••••">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label small text-muted">Dozvoljene države (ISO-2 kodovi)</label>
|
||||
<input id="countries-input" type="text" class="form-control"
|
||||
placeholder="HR BA SI RS AT DE">
|
||||
<small class="text-muted">Sve ostale države bit će blokirane</small>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button class="btn btn-success" onclick="enableGeoIP()">
|
||||
<i class="bi bi-globe-americas"></i> Aktiviraj GeoIP blokiranje
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Referenca ISO kodova -->
|
||||
<div class="card mt-3">
|
||||
<div class="card-header small text-muted">Česti ISO-2 kodovi</div>
|
||||
<div class="card-body">
|
||||
{% for cc, name in [('HR','Hrvatska'),('BA','Bosna i Hercegovina'),('SI','Slovenija'),
|
||||
('RS','Srbija'),('ME','Crna Gora'),('MK','Sjeverna Makedonija'),
|
||||
('AT','Austrija'),('DE','Njemačka'),('CH','Švicarska')] %}
|
||||
<span class="badge-country" style="cursor:pointer"
|
||||
onclick="addCountry('{{ cc }}')" title="{{ name }}">
|
||||
{{ cc }} <small class="text-muted">{{ name }}</small>
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
async function enableGeoIP() {
|
||||
const btn = event.target;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Aktivacija...';
|
||||
const r = await api('/api/geoip/enable', {
|
||||
mm_id: document.getElementById('mm-id').value.trim(),
|
||||
mm_key: document.getElementById('mm-key').value.trim(),
|
||||
countries: document.getElementById('countries-input').value.trim()
|
||||
});
|
||||
showToast(r.msg, r.ok);
|
||||
if (r.ok) setTimeout(() => location.reload(), 1500);
|
||||
else { btn.disabled = false; btn.innerHTML = '<i class="bi bi-globe-americas"></i> Aktiviraj GeoIP blokiranje'; }
|
||||
}
|
||||
|
||||
async function updateCountries() {
|
||||
const r = await api('/api/geoip/update', {
|
||||
countries: document.getElementById('countries-input').value.trim()
|
||||
});
|
||||
showToast(r.msg, r.ok);
|
||||
if (r.ok) setTimeout(() => location.reload(), 1000);
|
||||
}
|
||||
|
||||
async function disableGeoIP() {
|
||||
if (!confirm('Sigurno onemogućiti GeoIP?')) return;
|
||||
const r = await api('/api/geoip/disable');
|
||||
showToast(r.msg, r.ok);
|
||||
if (r.ok) setTimeout(() => location.reload(), 1000);
|
||||
}
|
||||
|
||||
async function refreshDB() {
|
||||
const btn = event.target;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Ažuriranje...';
|
||||
const r = await api('/api/geoip/refresh');
|
||||
showToast(r.ok ? 'GeoIP baza ažurirana' : r.msg, r.ok);
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-cloud-download"></i> Ažuriraj GeoIP bazu';
|
||||
}
|
||||
|
||||
function addCountry(cc) {
|
||||
const inp = document.getElementById('countries-input');
|
||||
const existing = inp.value.split(' ').filter(x => x);
|
||||
if (!existing.includes(cc)) {
|
||||
inp.value = [...existing, cc].join(' ');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,132 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Kontejneri – Guacamole Manager{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h4 class="mb-4"><i class="bi bi-boxes me-2"></i>Docker kontejneri</h4>
|
||||
|
||||
<!-- Status kartice -->
|
||||
<div class="row g-3 mb-4">
|
||||
{% for name, running in containers.items() %}
|
||||
<div class="col-md-4">
|
||||
<div class="card p-3">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<span class="status-dot {{ 'status-ok' if running else 'status-err' }}"></span>
|
||||
<div>
|
||||
<div class="fw-semibold">{{ name }}</div>
|
||||
<small class="{{ 'text-success' if running else 'text-danger' }}">
|
||||
{{ 'Aktivan' if running else 'Nije aktivan' }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Akcije -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header fw-semibold">Upravljanje</div>
|
||||
<div class="card-body d-flex gap-2 flex-wrap">
|
||||
<button class="btn btn-success" onclick="containerAction('start')">
|
||||
<i class="bi bi-play-fill"></i> Start
|
||||
</button>
|
||||
<button class="btn btn-warning" onclick="containerAction('restart')">
|
||||
<i class="bi bi-arrow-clockwise"></i> Restart
|
||||
</button>
|
||||
<button class="btn btn-danger" onclick="containerAction('stop')">
|
||||
<i class="bi bi-stop-fill"></i> Stop
|
||||
</button>
|
||||
<div class="ms-auto">
|
||||
<select id="log-source" class="form-select form-select-sm d-inline-block w-auto"
|
||||
onchange="switchLog(this.value)">
|
||||
<option value="guacamole">guacamole</option>
|
||||
<option value="guacd">guacd</option>
|
||||
<option value="postgres">postgres</option>
|
||||
<option value="nginx">nginx access</option>
|
||||
<option value="nginx-error">nginx error</option>
|
||||
<option value="fail2ban">fail2ban</option>
|
||||
</select>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1" onclick="toggleLog()">
|
||||
<i class="bi bi-terminal" id="log-icon"></i>
|
||||
<span id="log-btn-txt">Log</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Live log -->
|
||||
<div id="log-panel" class="card d-none">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span class="fw-semibold">
|
||||
<i class="bi bi-terminal me-1"></i>
|
||||
Live log: <span id="log-title">guacamole</span>
|
||||
</span>
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="clearLog()">
|
||||
<i class="bi bi-trash"></i> Očisti
|
||||
</button>
|
||||
</div>
|
||||
<div id="log-output"></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
let logSource = null;
|
||||
let logVisible = false;
|
||||
|
||||
async function containerAction(action) {
|
||||
const btn = event.target.closest('button');
|
||||
const orig = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = `<span class="spinner-border spinner-border-sm"></span> ${action}...`;
|
||||
|
||||
const r = await api(`/api/guacamole/${action}`);
|
||||
showToast(r.ok ? `${action} izvršen` : r.msg, r.ok);
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = orig;
|
||||
if (r.ok) setTimeout(() => location.reload(), 2000);
|
||||
}
|
||||
|
||||
function switchLog(service) {
|
||||
if (logVisible) {
|
||||
stopLog();
|
||||
startLog(service);
|
||||
}
|
||||
document.getElementById('log-title').textContent = service;
|
||||
}
|
||||
|
||||
function toggleLog() {
|
||||
if (logVisible) {
|
||||
stopLog();
|
||||
logVisible = false;
|
||||
document.getElementById('log-panel').classList.add('d-none');
|
||||
document.getElementById('log-btn-txt').textContent = 'Log';
|
||||
} else {
|
||||
logVisible = true;
|
||||
document.getElementById('log-panel').classList.remove('d-none');
|
||||
document.getElementById('log-btn-txt').textContent = 'Zatvori';
|
||||
startLog(document.getElementById('log-source').value);
|
||||
}
|
||||
}
|
||||
|
||||
function startLog(service) {
|
||||
stopLog();
|
||||
const out = document.getElementById('log-output');
|
||||
logSource = new EventSource(`/api/logs/${service}`);
|
||||
logSource.onmessage = e => {
|
||||
const line = document.createElement('div');
|
||||
line.textContent = JSON.parse(e.data);
|
||||
out.appendChild(line);
|
||||
out.scrollTop = out.scrollHeight;
|
||||
};
|
||||
}
|
||||
|
||||
function stopLog() {
|
||||
if (logSource) { logSource.close(); logSource = null; }
|
||||
}
|
||||
|
||||
function clearLog() {
|
||||
document.getElementById('log-output').innerHTML = '';
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,55 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="hr" data-bs-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Login – Guacamole Manager</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css">
|
||||
<style>
|
||||
body { background: #0f1117; min-height: 100vh;
|
||||
display: flex; align-items: center; justify-content: center; }
|
||||
.login-card { width: 360px; background: #161b22;
|
||||
border: 1px solid #30363d; border-radius: 12px; padding: 2rem; }
|
||||
.brand { color: #58a6ff; font-size: 1.4rem; font-weight: 700; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-card">
|
||||
<div class="text-center mb-4">
|
||||
<i class="bi bi-display fs-1 text-primary"></i>
|
||||
<div class="brand mt-1">Guacamole Manager</div>
|
||||
<small class="text-muted">Web upravljačko sučelje</small>
|
||||
</div>
|
||||
|
||||
{% with msgs = get_flashed_messages(with_categories=true) %}
|
||||
{% for cat, msg in msgs %}
|
||||
<div class="alert alert-{{ cat }} py-2 small">{{ msg }}</div>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="post">
|
||||
<div class="mb-3">
|
||||
<label class="form-label small text-muted">Korisničko ime</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="bi bi-person"></i></span>
|
||||
<input name="username" type="text" class="form-control"
|
||||
placeholder="admin" autofocus required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="form-label small text-muted">Lozinka</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="bi bi-key"></i></span>
|
||||
<input name="password" type="password" class="form-control"
|
||||
placeholder="••••••••" required>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<i class="bi bi-box-arrow-in-right"></i> Prijava
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,97 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Postavke – Guacamole Manager{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h4 class="mb-4"><i class="bi bi-gear me-2"></i>Postavke</h4>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-md-5">
|
||||
<div class="card">
|
||||
<div class="card-header fw-semibold">
|
||||
<i class="bi bi-key me-1"></i> Promjena lozinke GUI-ja
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label small text-muted">Korisničko ime</label>
|
||||
<input type="text" class="form-control" value="{{ username }}" readonly
|
||||
style="background:#0d1117; opacity:.7">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small text-muted">Trenutna lozinka</label>
|
||||
<input id="cur-pw" type="password" class="form-control" placeholder="••••••••">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small text-muted">Nova lozinka</label>
|
||||
<input id="new-pw" type="password" class="form-control" placeholder="Min. 8 znakova">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small text-muted">Potvrdi novu lozinku</label>
|
||||
<input id="new-pw2" type="password" class="form-control" placeholder="••••••••">
|
||||
</div>
|
||||
<button class="btn btn-primary w-100" onclick="changePassword()">
|
||||
<i class="bi bi-floppy"></i> Spremi lozinku
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-7">
|
||||
<div class="card">
|
||||
<div class="card-header fw-semibold">
|
||||
<i class="bi bi-info-circle me-1"></i> Informacije
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="row">
|
||||
<dt class="col-5 text-muted small">Instalacijski dir</dt>
|
||||
<dd class="col-7 font-monospace small">/opt/guacamole</dd>
|
||||
<dt class="col-5 text-muted small">Nginx konfig</dt>
|
||||
<dd class="col-7 font-monospace small">/etc/nginx/.../guacamole</dd>
|
||||
<dt class="col-5 text-muted small">Basic Auth file</dt>
|
||||
<dd class="col-7 font-monospace small">/etc/nginx/.guac_htpasswd</dd>
|
||||
<dt class="col-5 text-muted small">GeoIP konfig</dt>
|
||||
<dd class="col-7 font-monospace small">/etc/nginx/conf.d/guacamole-geoip.conf</dd>
|
||||
<dt class="col-5 text-muted small">fail2ban jail</dt>
|
||||
<dd class="col-7 font-monospace small">/etc/fail2ban/jail.d/guacamole.conf</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header fw-semibold">
|
||||
<i class="bi bi-terminal me-1"></i> Brze naredbe (u terminalu)
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<pre class="small mb-0" style="background:#0d1117; border:1px solid #30363d; padding:.75rem; border-radius:6px; color:#8b949e"># Logovi Guacamolua
|
||||
docker logs -f guacamole
|
||||
|
||||
# Restart svih kontejnera
|
||||
cd /opt/guacamole && docker compose restart
|
||||
|
||||
# fail2ban status
|
||||
fail2ban-client status
|
||||
|
||||
# Backup konfiguracije
|
||||
sudo bash /path/to/manage.sh</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
async function changePassword() {
|
||||
const cur = document.getElementById('cur-pw').value;
|
||||
const np = document.getElementById('new-pw').value;
|
||||
const np2 = document.getElementById('new-pw2').value;
|
||||
if (np !== np2) return showToast('Lozinke se ne podudaraju', false);
|
||||
if (np.length < 8) return showToast('Lozinka mora imati najmanje 8 znakova', false);
|
||||
const r = await api('/api/settings/password', {current: cur, new_password: np});
|
||||
showToast(r.msg, r.ok);
|
||||
if (r.ok) {
|
||||
['cur-pw','new-pw','new-pw2'].forEach(id =>
|
||||
document.getElementById(id).value = '');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,159 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}SSL{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h4 class="mb-0"><i class="bi bi-lock me-2"></i>SSL certifikat</h4>
|
||||
{% if active %}
|
||||
<span class="badge bg-success fs-6">Aktivan</span>
|
||||
{% else %}
|
||||
<span class="badge bg-warning text-dark fs-6">Nije aktivan</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if active %}
|
||||
<div class="row g-4">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header fw-semibold">Informacije</div>
|
||||
<div class="card-body">
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-5 text-muted small">Domena / IP</dt>
|
||||
<dd class="col-7">{{ domain }}</dd>
|
||||
</dl>
|
||||
{% if cert_info %}
|
||||
<pre class="mt-2 p-2 rounded small text-muted"
|
||||
style="background:#0d1117;border:1px solid #30363d;font-size:.75rem;max-height:200px;overflow:auto">{{ cert_info }}</pre>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header fw-semibold">Akcije</div>
|
||||
<div class="card-body d-flex flex-column gap-2">
|
||||
<button class="btn btn-outline-primary" onclick="renewSSL()">
|
||||
<i class="bi bi-arrow-repeat"></i> Obnovi Let's Encrypt certifikat
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" onclick="loadSelfSignedInfo()">
|
||||
<i class="bi bi-info-circle"></i> Provjeri self-signed info
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<pre id="ss-info" class="mt-3 p-3 rounded small d-none"
|
||||
style="background:#0d1117;border:1px solid #30363d;color:#8b949e"></pre>
|
||||
|
||||
{% else %}
|
||||
<div class="row g-4">
|
||||
|
||||
<!-- Let's Encrypt -->
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header fw-semibold">
|
||||
<i class="bi bi-globe me-1"></i> Let's Encrypt
|
||||
<span class="badge bg-success ms-1 small">Preporučeno</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="small text-muted mb-3">
|
||||
<li>Besplatan, povjerljiv certifikat</li>
|
||||
<li>Browser ne pokazuje upozorenje</li>
|
||||
<li>Zahtijeva javnu domenu (ne radi s IP-om)</li>
|
||||
<li>Auto-obnova svakih 90 dana</li>
|
||||
</ul>
|
||||
<div class="alert alert-warning small py-2">
|
||||
<i class="bi bi-exclamation-triangle me-1"></i>
|
||||
DNS mora pokazivati na ovaj server.
|
||||
</div>
|
||||
<div class="row g-2 mt-1">
|
||||
<div class="col-12">
|
||||
<input id="ssl-domain" type="text" class="form-control form-control-sm"
|
||||
value="{{ domain }}" placeholder="rdp.firma.com">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<input id="ssl-email" type="email" class="form-control form-control-sm"
|
||||
placeholder="admin@firma.com">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button class="btn btn-success w-100" onclick="enableSSL()">
|
||||
<i class="bi bi-lock"></i> Aktiviraj Let's Encrypt
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Self-signed -->
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header fw-semibold">
|
||||
<i class="bi bi-shield-lock me-1"></i> Self-signed certifikat
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="small text-muted mb-3">
|
||||
<li>Radi s IP adresom ili domenom</li>
|
||||
<li>Enkriptiran promet (HTTPS)</li>
|
||||
<li>Browser pokazuje upozorenje pri prvom posjetu</li>
|
||||
<li>Vrijedi 10 godina, bez potrebe za obnovom</li>
|
||||
</ul>
|
||||
<div class="alert alert-info small py-2">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
Idealno za internu mrežu ili testiranje.<br>
|
||||
Generirat ce se za: <strong>{{ domain }}</strong>
|
||||
</div>
|
||||
<button class="btn btn-outline-primary w-100 mt-2" onclick="enableSelfSigned()">
|
||||
<i class="bi bi-shield-lock"></i> Generiraj self-signed certifikat
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
async function enableSSL() {
|
||||
const btn = event.target;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Aktivacija (moze potrajati)...';
|
||||
const r = await api('/api/ssl/enable', {
|
||||
domain: document.getElementById('ssl-domain').value.trim(),
|
||||
email: document.getElementById('ssl-email').value.trim()
|
||||
});
|
||||
showToast(r.ok ? 'SSL aktiviran!' : r.msg, r.ok);
|
||||
if (r.ok) setTimeout(() => location.reload(), 2000);
|
||||
else { btn.disabled = false; btn.innerHTML = '<i class="bi bi-lock"></i> Aktiviraj Let\'s Encrypt'; }
|
||||
}
|
||||
|
||||
async function enableSelfSigned() {
|
||||
const btn = event.target;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Generiranje...';
|
||||
const r = await api('/api/ssl/selfsigned');
|
||||
showToast(r.msg, r.ok);
|
||||
if (r.ok) setTimeout(() => location.reload(), 1500);
|
||||
else { btn.disabled = false; btn.innerHTML = '<i class="bi bi-shield-lock"></i> Generiraj self-signed certifikat'; }
|
||||
}
|
||||
|
||||
async function renewSSL() {
|
||||
const btn = event.target;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Obnavljanje...';
|
||||
const r = await api('/api/ssl/renew');
|
||||
showToast(r.ok ? 'Certifikat obnovljen' : r.msg, r.ok);
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-arrow-repeat"></i> Obnovi Let\'s Encrypt certifikat';
|
||||
}
|
||||
|
||||
async function loadSelfSignedInfo() {
|
||||
const r = await fetch('/api/ssl/selfsigned_info').then(r => r.json());
|
||||
const el = document.getElementById('ss-info');
|
||||
el.textContent = r.info;
|
||||
el.classList.remove('d-none');
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,207 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Ažuriranja{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h4 class="mb-4"><i class="bi bi-arrow-up-circle me-2"></i>Ažuriranja</h4>
|
||||
|
||||
<!-- Trenutne verzije -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header fw-semibold"><i class="bi bi-info-circle me-1"></i> Trenutne verzije</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
{% for name, ver in versions.items() %}
|
||||
<div class="col-md-4">
|
||||
<div class="p-3 rounded" style="background:#0d1117; border:1px solid #30363d">
|
||||
<div class="small text-muted text-uppercase mb-1">{{ name }}</div>
|
||||
<div class="fw-semibold font-monospace">{{ ver }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Akcije ažuriranja -->
|
||||
<div class="row g-4">
|
||||
|
||||
<!-- Guacamole -->
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header fw-semibold">
|
||||
<i class="bi bi-display me-1"></i> Guacamole
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted small">
|
||||
Preuzima najnovije Docker image-e za Guacamole, guacd i PostgreSQL
|
||||
te restarta kontejnere. Konfiguracija i podaci se čuvaju.
|
||||
</p>
|
||||
<button class="btn btn-primary" onclick="startUpdate('guacamole', this)">
|
||||
<i class="bi bi-cloud-download"></i> Ažuriraj Guacamole
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Nginx -->
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header fw-semibold">
|
||||
<i class="bi bi-server me-1"></i> Nginx
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted small">
|
||||
Ažurira nginx paket putem sistemskog package managera
|
||||
({{ pkg }}) i automatski reloada konfiguraciju.
|
||||
</p>
|
||||
<button class="btn btn-primary" onclick="startUpdate('nginx', this)">
|
||||
<i class="bi bi-arrow-up-circle"></i> Ažuriraj Nginx
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- fail2ban -->
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header fw-semibold">
|
||||
<i class="bi bi-shield me-1"></i> fail2ban
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted small">
|
||||
Ažurira fail2ban paket i restarta servis.
|
||||
Jail konfiguracije ostaju netaknute.
|
||||
</p>
|
||||
<button class="btn btn-primary" onclick="updateFail2ban(this)">
|
||||
<i class="bi bi-arrow-up-circle"></i> Ažuriraj fail2ban
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sistem -->
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header fw-semibold">
|
||||
<i class="bi bi-hdd me-1"></i> Sistemske nadogradnje
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted small">
|
||||
Ažurira sve instalirane sistemske pakete putem <code>{{ pkg }}</code>.
|
||||
Može potrajati nekoliko minuta.
|
||||
</p>
|
||||
<div class="alert alert-warning py-2 small mb-3">
|
||||
<i class="bi bi-exclamation-triangle me-1"></i>
|
||||
Nakon sistemske nadogradnje može biti potreban restart servera.
|
||||
</div>
|
||||
<button class="btn btn-warning" onclick="startUpdate('system', this)">
|
||||
<i class="bi bi-hdd-rack"></i> Ažuriraj sistem
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Output konzola -->
|
||||
<div id="output-panel" class="card mt-4 d-none">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span class="fw-semibold">
|
||||
<i class="bi bi-terminal me-1"></i>
|
||||
Output: <span id="output-title">—</span>
|
||||
</span>
|
||||
<div>
|
||||
<span id="output-status" class="badge bg-warning me-2">U tijeku...</span>
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="clearOutput()">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="log-output"></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
let activeSource = null;
|
||||
|
||||
function startUpdate(type, btn) {
|
||||
const titles = {
|
||||
guacamole: 'Guacamole',
|
||||
nginx: 'Nginx',
|
||||
system: 'Sistemske nadogradnje'
|
||||
};
|
||||
|
||||
if (activeSource) {
|
||||
activeSource.close();
|
||||
activeSource = null;
|
||||
}
|
||||
|
||||
document.getElementById('output-panel').classList.remove('d-none');
|
||||
document.getElementById('output-title').textContent = titles[type] || type;
|
||||
document.getElementById('output-status').textContent = 'U tijeku...';
|
||||
document.getElementById('output-status').className = 'badge bg-warning me-2';
|
||||
|
||||
const out = document.getElementById('log-output');
|
||||
out.innerHTML = '';
|
||||
|
||||
const origHtml = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Ažuriranje...';
|
||||
|
||||
activeSource = new EventSource(`/api/update/${type}`);
|
||||
activeSource.onmessage = e => {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data === '__DONE__') {
|
||||
activeSource.close();
|
||||
activeSource = null;
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = origHtml;
|
||||
document.getElementById('output-status').textContent = 'Završeno';
|
||||
document.getElementById('output-status').className = 'badge bg-success me-2';
|
||||
return;
|
||||
}
|
||||
const line = document.createElement('div');
|
||||
line.textContent = data;
|
||||
if (data.startsWith('✓')) line.style.color = '#3fb950';
|
||||
out.appendChild(line);
|
||||
out.scrollTop = out.scrollHeight;
|
||||
};
|
||||
activeSource.onerror = () => {
|
||||
activeSource.close();
|
||||
activeSource = null;
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = origHtml;
|
||||
document.getElementById('output-status').textContent = 'Greška';
|
||||
document.getElementById('output-status').className = 'badge bg-danger me-2';
|
||||
};
|
||||
}
|
||||
|
||||
async function updateFail2ban(btn) {
|
||||
const orig = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Ažuriranje...';
|
||||
|
||||
document.getElementById('output-panel').classList.remove('d-none');
|
||||
document.getElementById('output-title').textContent = 'fail2ban';
|
||||
document.getElementById('output-status').textContent = 'U tijeku...';
|
||||
document.getElementById('output-status').className = 'badge bg-warning me-2';
|
||||
|
||||
const r = await api('/api/update/fail2ban');
|
||||
const out = document.getElementById('log-output');
|
||||
out.innerHTML = '';
|
||||
const line = document.createElement('div');
|
||||
line.textContent = r.msg;
|
||||
out.appendChild(line);
|
||||
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = orig;
|
||||
document.getElementById('output-status').textContent = r.ok ? 'Završeno' : 'Greška';
|
||||
document.getElementById('output-status').className = `badge ${r.ok ? 'bg-success' : 'bg-danger'} me-2`;
|
||||
showToast(r.ok ? 'fail2ban ažuriran' : r.msg, r.ok);
|
||||
}
|
||||
|
||||
function clearOutput() {
|
||||
document.getElementById('log-output').innerHTML = '';
|
||||
document.getElementById('output-panel').classList.add('d-none');
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user