From 149ea6f8f3c3dce638de764efdaeb6f775806994 Mon Sep 17 00:00:00 2001 From: njomac Date: Tue, 28 Jul 2026 20:36:00 +0200 Subject: [PATCH] refactor: install.sh deploya webgui/ umjesto inline kopije install.sh je nosio vlastitu kopiju Web GUI-ja u heredoc blokovima i ta se kopija razisla s webgui/ direktorijem: inline verzija imala je 8 stranica, webgui/ ih ima 10. Stranice Backup i Azuriranja, koje README i UPUTE.txt dokumentiraju, tako nikad nisu zavrsile na serveru. Sada se app.py i predlosci kopiraju iz webgui/ pokraj skripte (SRC_DIR). Uklonjeno ~1090 linija duplikata. Ako webgui/ nedostaje, skripta pukne s jasnom porukom umjesto da instalira nepotpun GUI. README i UPUTE dopunjeni: treba kopirati cijeli repo, ne samo install.sh. Provjereno cistim installom na Ubuntu 24.04: svih 9 stranica vraca 200, backup stvarno kreira arhivu s pg_dumpom i nginx/ssl konfiguracijom, TOTP se ucitava, sve prezivi reboot. Co-Authored-By: Claude Opus 5 --- README.md | 3 + UPUTE.txt | 5 +- install.sh | 1105 +--------------------------------------------------- 3 files changed, 20 insertions(+), 1093 deletions(-) diff --git a/README.md b/README.md index 5a5810b..fa7e7e2 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,9 @@ Ubuntu · Debian · RHEL · CentOS · Rocky Linux · AlmaLinux · Fedora · Arch ## Instalacija +Kopirajte **cijeli repo** na server (installer uzima Web GUI iz `webgui/`, +pa sama `install.sh` nije dovoljna), pa pokrenite: + ```bash sudo bash install.sh ``` diff --git a/UPUTE.txt b/UPUTE.txt index a145f9c..472d8e3 100644 --- a/UPUTE.txt +++ b/UPUTE.txt @@ -55,10 +55,13 @@ Podržani operativni sustavi: 3. INSTALACIJA ================================================================ -Kopirati datoteke na server i pokrenuti: +Kopirati CIJELI repo na server i pokrenuti: sudo bash install.sh +Installer uzima Web GUI iz webgui/ direktorija pokraj sebe, pa +kopiranje same install.sh nije dovoljno — skripta ce javiti gresku. + Skripta je potpuno automatizirana — sve instalira sama. Nema potrebe pokretati ništa drugo nakon toga. diff --git a/install.sh b/install.sh index 5de9d02..da79b2c 100755 --- a/install.sh +++ b/install.sh @@ -22,6 +22,9 @@ GUAC_VERSION="1.5.5" GUAC_PORT="8080" INSTALL_DIR="/opt/guacamole" +# Direktorij repoa — Web GUI se kopira iz webgui/ pokraj ove skripte. +SRC_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" + # ── OS detekcija ────────────────────────────────────────────────────────────── section "Detekcija operativnog sustava" @@ -625,1098 +628,16 @@ if [[ "$DO_WEBGUI" =~ ^[Yy]$ ]]; then esac info "Python: $(python3 --version)" - # ── app.py ────────────────────────────────────────────────── - cat > "${WEBGUI_DIR}/app.py" << 'APPEOF' -#!/usr/bin/env python3 -import os, subprocess, json, secrets, shutil, re -from functools import wraps -from flask import (Flask, render_template, request, redirect, - url_for, session, jsonify, flash, Response, stream_with_context) - -BASE_DIR = os.path.dirname(os.path.abspath(__file__)) -INSTALL_DIR = '/opt/guacamole' -HTPASSWD = '/etc/nginx/.guac_htpasswd' -GEOIP_CONF = '/etc/nginx/conf.d/guacamole-geoip.conf' -GEOIP_MMCONF = '/etc/GeoIP.conf' -FAIL2BAN_CFG = '/etc/fail2ban/jail.d/guacamole.conf' -CONFIG_FILE = os.path.join(BASE_DIR, 'config.json') -NGINX_CONFS = ['/etc/nginx/sites-available/guacamole', - '/etc/nginx/conf.d/guacamole.conf'] - -app = Flask(__name__, template_folder='templates') -app.secret_key = os.environ.get('SECRET_KEY', secrets.token_hex(32)) - - -class PrefixMiddleware: - """Nginx proxira GUI_PATH (npr. /guacadmin/) na Flask koji slusa na - rootu. Bez SCRIPT_NAME-a url_for i redirecti generiraju putanje izvan - prefiksa (/login umjesto /guacadmin/login) i nginx vraca 404.""" - - def __init__(self, wsgi_app, prefix=''): - self.wsgi_app = wsgi_app - prefix = (prefix or '').strip('/') - self.prefix = '/' + prefix if prefix else '' - - def __call__(self, environ, start_response): - if self.prefix: - environ['SCRIPT_NAME'] = self.prefix - path = environ.get('PATH_INFO', '') - if path.startswith(self.prefix): - environ['PATH_INFO'] = path[len(self.prefix):] or '/' - return self.wsgi_app(environ, start_response) - - -app.wsgi_app = PrefixMiddleware(app.wsgi_app, os.environ.get('GUI_PATH', '')) - -def load_config(): - if os.path.exists(CONFIG_FILE): - with open(CONFIG_FILE) as f: - return json.load(f) - return {'username': 'admin', 'password': 'changeme123'} - -def save_config(cfg): - with open(CONFIG_FILE, 'w') as f: - json.dump(cfg, f, indent=2) - os.chmod(CONFIG_FILE, 0o600) - -def run(cmd, stdin=None, timeout=30): - try: - r = subprocess.run(cmd, capture_output=True, text=True, - timeout=timeout, input=stdin) - return r.returncode == 0, (r.stdout + r.stderr).strip() - except subprocess.TimeoutExpired: - return False, 'Timeout' - except Exception as e: - return False, str(e) - -def detect_compose(): - ok, _ = run(['docker', 'compose', 'version']) - return 'docker compose' if ok else 'docker-compose' - -COMPOSE = detect_compose().split() - -def get_nginx_conf(): - for p in NGINX_CONFS: - if os.path.exists(p): - return p - return None - -def nginx_reload(): - ok, out = run(['nginx', '-t']) - if ok: - run(['systemctl', 'reload', 'nginx']) - return ok, out - -def container_running(name): - ok, out = run(['docker', 'ps', '--filter', f'name=^/{name}$', - '--filter', 'status=running', '--format', '{{.Names}}']) - return name in out - -def service_active(name): - ok, _ = run(['systemctl', 'is-active', name]) - return ok - -def get_domain(): - conf = get_nginx_conf() - if conf and os.path.exists(conf): - with open(conf) as f: - for line in f: - m = re.search(r'server_name\s+(.+?);', line) - if m: - return m.group(1).strip() - return 'localhost' - -def geoip_countries(): - if not os.path.exists(GEOIP_CONF): - return [] - with open(GEOIP_CONF) as f: - return re.findall(r'^\s+([A-Z]{2})\s+1;', f.read(), re.MULTILINE) - -def fail2ban_banned(jail): - ok, out = run(['fail2ban-client', 'status', jail]) - if not ok: - return [] - m = re.search(r'Banned IP list:\s*(.*)', out) - if m: - return [ip for ip in m.group(1).split() if ip] - return [] - -def htpasswd_users(): - if not os.path.exists(HTPASSWD): - return [] - with open(HTPASSWD) as f: - return [line.split(':')[0] for line in f if ':' in line] - -def ssl_active(): - conf = get_nginx_conf() - if conf and os.path.exists(conf): - with open(conf) as f: - return 'ssl_certificate' in f.read() - return False - -def login_required(f): - @wraps(f) - def inner(*args, **kwargs): - if not session.get('logged_in'): - return redirect(url_for('login', next=request.path)) - return f(*args, **kwargs) - return inner - -def api_auth(f): - @wraps(f) - def inner(*args, **kwargs): - if not session.get('logged_in'): - return jsonify(ok=False, msg='Unauthorized'), 401 - return f(*args, **kwargs) - return inner - -@app.route('/') -@login_required -def index(): - return redirect(url_for('dashboard')) - -@app.route('/login', methods=['GET', 'POST']) -def login(): - if request.method == 'POST': - cfg = load_config() - if (request.form.get('username') == cfg['username'] and - request.form.get('password') == cfg['password']): - session.permanent = True - session['logged_in'] = True - # 'next' je PATH_INFO bez prefiksa; bez script_roota redirect - # izlazi izvan GUI_PATH-a i korisnik zavrsi na Guacamole loginu. - target = request.args.get('next') or '' - if not target.startswith('/') or target.startswith('//'): - target = '' - return redirect(request.script_root + target if target - else url_for('dashboard')) - flash('Pogrešno korisničko ime ili lozinka', 'danger') - return render_template('login.html') - -@app.route('/logout') -def logout(): - session.clear() - return redirect(url_for('login')) - -@app.route('/dashboard') -@login_required -def dashboard(): - return render_template('dashboard.html', domain=get_domain()) - -@app.route('/basicauth') -@login_required -def basicauth(): - return render_template('basicauth.html', users=htpasswd_users()) - -@app.route('/geoip') -@login_required -def geoip(): - enabled = os.path.exists(GEOIP_CONF) - countries = ' '.join(geoip_countries()) - mm_id = '' - if os.path.exists(GEOIP_MMCONF): - with open(GEOIP_MMCONF) as f: - txt = f.read() - m = re.search(r'AccountID\s+(\S+)', txt) - if m: - mm_id = m.group(1) - return render_template('geoip.html', enabled=enabled, - countries=countries, mm_id=mm_id) - -@app.route('/fail2ban') -@login_required -def fail2ban(): - jails = ['nginx-basicauth', 'nginx-guacamole', 'sshd'] - banned = {j: fail2ban_banned(j) for j in jails} - cfg = {} - if os.path.exists(FAIL2BAN_CFG): - with open(FAIL2BAN_CFG) as f: - txt = f.read() - m = re.search(r'maxretry\s*=\s*(\d+)', txt) - if m: - cfg['maxretry'] = m.group(1) - m = re.search(r'bantime\s*=\s*(\d+)', txt) - if m: - cfg['bantime'] = m.group(1) - return render_template('fail2ban.html', banned=banned, cfg=cfg) - -@app.route('/guacamole') -@login_required -def guacamole(): - containers = {n: container_running(n) - for n in ['guacamole', 'guacd', 'guac_postgres']} - return render_template('guacamole.html', containers=containers) - -@app.route('/ssl') -@login_required -def ssl(): - active = ssl_active() - domain = get_domain() - cert_info = '' - if active: - ok, out = run(['certbot', 'certificates']) - cert_info = out - return render_template('ssl.html', active=active, domain=domain, cert_info=cert_info) - -@app.route('/settings') -@login_required -def settings(): - cfg = load_config() - return render_template('settings.html', username=cfg['username']) - -@app.route('/api/status') -@api_auth -def api_status(): - containers = {n: container_running(n) - for n in ['guacamole', 'guacd', 'guac_postgres']} - services = {s: service_active(s) for s in ['nginx', 'fail2ban', 'docker']} - banned_total = sum(len(fail2ban_banned(j)) - for j in ['nginx-basicauth', 'nginx-guacamole', 'sshd']) - return jsonify(containers=containers, services=services, - geoip=os.path.exists(GEOIP_CONF), ssl=ssl_active(), - banned=banned_total, domain=get_domain()) - -@app.route('/api/basicauth/add', methods=['POST']) -@api_auth -def api_ba_add(): - user = request.json.get('username', '').strip() - pw = request.json.get('password', '') - if not re.match(r'^[a-zA-Z0-9_.-]+$', user): - return jsonify(ok=False, msg='Nevažeće korisničko ime') - if not pw: - return jsonify(ok=False, msg='Lozinka je obavezna') - flag = '-b' if os.path.exists(HTPASSWD) else '-bc' - ok, out = run(['htpasswd', flag, HTPASSWD, user, pw]) - if ok: - nginx_reload() - return jsonify(ok=ok, msg=out or 'Korisnik dodan') - -@app.route('/api/basicauth/update', methods=['POST']) -@api_auth -def api_ba_update(): - user = request.json.get('username', '').strip() - pw = request.json.get('password', '') - if not pw: - return jsonify(ok=False, msg='Lozinka je obavezna') - ok, out = run(['htpasswd', '-b', HTPASSWD, user, pw]) - if ok: - nginx_reload() - return jsonify(ok=ok, msg=out or 'Lozinka promijenjena') - -@app.route('/api/basicauth/delete', methods=['POST']) -@api_auth -def api_ba_delete(): - user = request.json.get('username', '').strip() - ok, out = run(['htpasswd', '-D', HTPASSWD, user]) - if ok: - nginx_reload() - return jsonify(ok=ok, msg=out or 'Korisnik uklonjen') - -def _write_geoip_conf(countries): - entries = ''.join(f' {c} 1;\n' for c in countries) - with open(GEOIP_CONF, 'w') as f: - f.write(f"geoip2 /usr/share/GeoIP/GeoLite2-Country.mmdb {{\n" - f" $geoip2_data_country_code country iso_code;\n}}\n\n" - f"map $geoip2_data_country_code $allowed_country {{\n" - f" default 0;\n{entries}}}\n") - -def _apply_geoip_nginx(): - conf = get_nginx_conf() - if not conf: - return - with open(conf) as f: - txt = f.read() - txt = re.sub(r'\s*if \(\$allowed_country = 0\).*?\n', '', txt) - txt = re.sub(r'(auth_basic\s+"Restricted[^"]*";)', - r'if ($allowed_country = 0) { return 403; }\n \1', txt) - with open(conf, 'w') as f: - f.write(txt) - -def _remove_geoip_nginx(): - conf = get_nginx_conf() - if not conf: - return - with open(conf) as f: - txt = f.read() - txt = re.sub(r'[ \t]*if \(\$allowed_country = 0\)[^\n]*\n', '', txt) - with open(conf, 'w') as f: - f.write(txt) - -@app.route('/api/geoip/enable', methods=['POST']) -@api_auth -def api_geoip_enable(): - data = request.json - mm_id = data.get('mm_id', '').strip() - mm_key = data.get('mm_key', '').strip() - countries = [c.upper() for c in data.get('countries', '').split() if c] - if not (mm_id and mm_key and countries): - return jsonify(ok=False, msg='Popunite sva polja') - if not shutil.which('geoipupdate'): - ok, out = run(['apt-get', 'install', '-y', 'geoipupdate', 'libnginx-mod-http-geoip2']) - if not ok: - return jsonify(ok=False, msg=f'Instalacija: {out}') - with open(GEOIP_MMCONF, 'w') as f: - f.write(f"AccountID {mm_id}\nLicenseKey {mm_key}\n" - f"EditionIDs GeoLite2-Country\nDatabaseDirectory /usr/share/GeoIP\n") - ok, out = run(['geoipupdate']) - if not ok: - return jsonify(ok=False, msg=f'geoipupdate: {out}') - _write_geoip_conf(countries) - _apply_geoip_nginx() - nginx_reload() - return jsonify(ok=True, msg='GeoIP aktiviran') - -@app.route('/api/geoip/update', methods=['POST']) -@api_auth -def api_geoip_update(): - countries = [c.upper() for c in request.json.get('countries', '').split() if c] - if not countries: - return jsonify(ok=False, msg='Unesite barem jednu zemlju') - _write_geoip_conf(countries) - ok, out = nginx_reload() - return jsonify(ok=ok, msg='Ažurirane dozvoljene države' if ok else out) - -@app.route('/api/geoip/disable', methods=['POST']) -@api_auth -def api_geoip_disable(): - if os.path.exists(GEOIP_CONF): - os.remove(GEOIP_CONF) - _remove_geoip_nginx() - nginx_reload() - return jsonify(ok=True, msg='GeoIP onemogućen') - -@app.route('/api/geoip/refresh', methods=['POST']) -@api_auth -def api_geoip_refresh(): - ok, out = run(['geoipupdate']) - return jsonify(ok=ok, msg=out) - -@app.route('/api/fail2ban/unban', methods=['POST']) -@api_auth -def api_unban(): - ip = request.json.get('ip', '').strip() - jail = request.json.get('jail', 'nginx-basicauth') - if not re.match(r'^[\d.:a-fA-F]+$', ip): - return jsonify(ok=False, msg='Nevažeća IP adresa') - ok, out = run(['fail2ban-client', 'set', jail, 'unbanip', ip]) - return jsonify(ok=ok, msg=out or f'{ip} odbanjivan') - -@app.route('/api/fail2ban/ban', methods=['POST']) -@api_auth -def api_ban(): - ip = request.json.get('ip', '').strip() - jail = request.json.get('jail', 'nginx-basicauth') - if not re.match(r'^[\d.:a-fA-F]+$', ip): - return jsonify(ok=False, msg='Nevažeća IP adresa') - ok, out = run(['fail2ban-client', 'set', jail, 'banip', ip]) - return jsonify(ok=ok, msg=out or f'{ip} baniran') - -@app.route('/api/fail2ban/settings', methods=['POST']) -@api_auth -def api_fail2ban_settings(): - maxretry = request.json.get('maxretry', '') - bantime = request.json.get('bantime', '') - if not (str(maxretry).isdigit() and str(bantime).lstrip('-').isdigit()): - return jsonify(ok=False, msg='Nevažeće vrijednosti') - if os.path.exists(FAIL2BAN_CFG): - with open(FAIL2BAN_CFG) as f: - txt = f.read() - txt = re.sub(r'maxretry\s*=\s*\d+', f'maxretry = {maxretry}', txt) - txt = re.sub(r'bantime\s*=\s*\d+', f'bantime = {bantime}', txt) - with open(FAIL2BAN_CFG, 'w') as f: - f.write(txt) - ok, out = run(['systemctl', 'restart', 'fail2ban']) - return jsonify(ok=ok, msg='Postavke ažurirane' if ok else out) - -@app.route('/api/guacamole/', methods=['POST']) -@api_auth -def api_guacamole(action): - if action not in ('start', 'stop', 'restart'): - return jsonify(ok=False, msg='Nevažeća akcija') - ok, out = run(COMPOSE + ['-f', f'{INSTALL_DIR}/docker-compose.yml', action], timeout=60) - return jsonify(ok=ok, msg=out or action.capitalize()) - -@app.route('/api/ssl/enable', methods=['POST']) -@api_auth -def api_ssl_enable(): - domain = request.json.get('domain', '').strip() - email = request.json.get('email', '').strip() - if not (domain and email): - return jsonify(ok=False, msg='Domena i email su obavezni') - if not shutil.which('certbot'): - ok, out = run(['apt-get', 'install', '-y', 'certbot', 'python3-certbot-nginx']) - if not ok: - return jsonify(ok=False, msg=f'Instalacija: {out}') - ok, out = run(['certbot', '--nginx', '-d', domain, '--email', email, - '--agree-tos', '--non-interactive', '--redirect'], timeout=120) - if ok: - nginx_reload() - return jsonify(ok=ok, msg=out) - -@app.route('/api/ssl/renew', methods=['POST']) -@api_auth -def api_ssl_renew(): - ok, out = run(['certbot', 'renew', '--force-renewal'], timeout=120) - if ok: - nginx_reload() - return jsonify(ok=ok, msg=out) - -@app.route('/api/nginx/reload', methods=['POST']) -@api_auth -def api_nginx_reload(): - ok, out = nginx_reload() - return jsonify(ok=ok, msg=out or 'Nginx reloadiran') - -@app.route('/api/settings/password', methods=['POST']) -@api_auth -def api_change_password(): - current = request.json.get('current', '') - new_pw = request.json.get('new_password', '') - cfg = load_config() - if cfg['password'] != current: - return jsonify(ok=False, msg='Pogrešna trenutna lozinka') - if len(new_pw) < 8: - return jsonify(ok=False, msg='Lozinka mora imati najmanje 8 znakova') - cfg['password'] = new_pw - save_config(cfg) - return jsonify(ok=True, msg='Lozinka promijenjena') - -@app.route('/api/logs/') -@login_required -def api_logs(service): - allowed = { - 'guacamole': ['docker', 'logs', '-f', '--tail=100', 'guacamole'], - 'guacd': ['docker', 'logs', '-f', '--tail=100', 'guacd'], - 'postgres': ['docker', 'logs', '-f', '--tail=100', 'guac_postgres'], - 'nginx': ['tail', '-f', '/var/log/nginx/access.log'], - 'nginx-error': ['tail', '-f', '/var/log/nginx/error.log'], - 'fail2ban': ['journalctl', '-fu', 'fail2ban', '-n', '100'], - } - if service not in allowed: - return jsonify(ok=False, msg='Nevažeći servis'), 400 - - def generate(): - proc = subprocess.Popen(allowed[service], stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, text=True) - try: - for line in proc.stdout: - yield f'data: {json.dumps(line.rstrip())}\n\n' - finally: - proc.terminate() - - return Response(stream_with_context(generate()), - mimetype='text/event-stream', - headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'}) - -if __name__ == '__main__': - port = int(os.environ.get('PORT', 5555)) - app.run(host='127.0.0.1', port=port, debug=False) -APPEOF - - # ── base.html ─────────────────────────────────────────────── - cat > "${WEBGUI_DIR}/templates/base.html" << 'HTMLEOF' - - - - - - {% block title %}Guacamole Manager{% endblock %} - - - - - - -
{% block content %}{% endblock %}
-
- -
- - -{% block scripts %}{% endblock %} - - -HTMLEOF - - # ── login.html ────────────────────────────────────────────── - cat > "${WEBGUI_DIR}/templates/login.html" << 'HTMLEOF' - - - - - Login – Guacamole Manager - - - - - - - - - -HTMLEOF - - # ── dashboard.html ────────────────────────────────────────── - cat > "${WEBGUI_DIR}/templates/dashboard.html" << 'HTMLEOF' -{% extends 'base.html' %} -{% block title %}Dashboard – Guacamole Manager{% endblock %} -{% block content %} -
-

Dashboard

{{ domain }}
- -
-
-
Docker kontejneri
- {% for name, label in [('guacamole','Guacamole Web'),('guacd','Guacd Daemon'),('guac_postgres','PostgreSQL')] %} -
-
-
- -
{{ label }}
Učitavanje...
-
-
-
- {% endfor %} -
-
-
Sistemski servisi
- {% for name, label in [('nginx','Nginx'),('fail2ban','fail2ban'),('docker','Docker')] %} -
-
-
- -
{{ label }}
Učitavanje...
-
-
-
- {% endfor %} -
-
-
-
- -
GeoIP blokiranje
...
-
-
-
-
- -
SSL certifikat
...
-
-
-
-
- -
Zabanjeni IP-ovi
...
-
-
-
-{% endblock %} -{% block scripts %} - -{% endblock %} -HTMLEOF - - # ── basicauth.html ────────────────────────────────────────── - cat > "${WEBGUI_DIR}/templates/basicauth.html" << 'HTMLEOF' -{% extends 'base.html' %} -{% block title %}Basic Auth{% endblock %} -{% block content %} -

Basic Auth korisnici

-
-
-
-
Korisnici{{ users|length }}
-
    - {% for user in users %} -
  • - {{ user }} -
    - - -
    -
  • - {% else %} -
  • Nema korisnika
  • - {% endfor %} -
-
-
-
-
-
Dodaj korisnika
-
-
-
-
-
-
-
-
-
-
Promijeni lozinku
-
-
-
-
-
-
-
-
-
-
-{% endblock %} -{% block scripts %} - -{% endblock %} -HTMLEOF - - # ── geoip.html ────────────────────────────────────────────── - cat > "${WEBGUI_DIR}/templates/geoip.html" << 'HTMLEOF' -{% extends 'base.html' %} -{% block title %}GeoIP{% endblock %} -{% block content %} -
-

GeoIP blokiranje

- {% if enabled %}Aktivno{% else %}Neaktivno{% endif %} -
-{% if enabled %} -
-
-
-
Dozvoljene države
-
-
{% for cc in countries.split() %}{{ cc }}{% endfor %}
-
- - -
- ISO-2 kodovi, razdvojeni razmakom -
-
-
-
-
-
Akcije
-
- - -
-
-
-
-{% else %} -
-
Konfiguracija GeoIP
-
-
Potreban besplatni MaxMind račun: maxmind.com
-
-
-
-
-
-
-
-
-
-
Česti ISO-2 kodovi (klikni za dodavanje)
-
- {% for cc, name in [('HR','Hrvatska'),('BA','Bosna i Hercegovina'),('SI','Slovenija'),('RS','Srbija'),('ME','Crna Gora'),('AT','Austrija'),('DE','Njemačka')] %} - {{ cc }} {{ name }} - {% endfor %} -
-
-{% endif %} -{% endblock %} -{% block scripts %} - -{% endblock %} -HTMLEOF - - # ── fail2ban.html ─────────────────────────────────────────── - cat > "${WEBGUI_DIR}/templates/fail2ban.html" << 'HTMLEOF' -{% extends 'base.html' %} -{% block title %}fail2ban{% endblock %} -{% block content %} -

fail2ban

-
-
- {% for jail, ips in banned.items() %} -
-
{{ jail }}{{ ips|length }} zabanjeno
-
- {% if ips %} - - - - {% for ip in ips %} - - {% endfor %} - -
IP adresaAkcija
{{ ip }}
- {% else %}

Nema zabanjenih IP-ova

{% endif %} -
-
- {% endfor %} -
-
-
-
Ručni ban
-
-
- - -
-
-
-
Postavke
-
-
-
3600=1h · 86400=1dan
- -
-
-
-
-{% endblock %} -{% block scripts %} - -{% endblock %} -HTMLEOF - - # ── guacamole.html ────────────────────────────────────────── - cat > "${WEBGUI_DIR}/templates/guacamole.html" << 'HTMLEOF' -{% extends 'base.html' %} -{% block title %}Kontejneri{% endblock %} -{% block content %} -

Docker kontejneri

-
- {% for name, running in containers.items() %} -
-
-
- -
{{ name }}
{{ 'Aktivan' if running else 'Nije aktivan' }}
-
-
-
- {% endfor %} -
-
-
Upravljanje
-
- - - -
- - -
-
-
-
-
- Live log: guacamole - -
-
-
-{% endblock %} -{% block scripts %} - -{% endblock %} -HTMLEOF - - # ── ssl.html ──────────────────────────────────────────────── - cat > "${WEBGUI_DIR}/templates/ssl.html" << 'HTMLEOF' -{% extends 'base.html' %} -{% block title %}SSL{% endblock %} -{% block content %} -
-

SSL certifikat

- {% if active %}Aktivan{% else %}Nije aktivan{% endif %} -
-{% if active %} -
-
-
-
Informacije
-
-
Domena
{{ domain }}
- {% if cert_info %}
{{ cert_info }}
{% endif %} -
-
-
-
-
Akcije
- -
-
-
-{% else %} -
-
Aktivacija Let's Encrypt SSL
-
-
Domena mora biti javno dostupna i DNS mora pokazivati na ovaj server.
-
-
-
-
-
-
-
-{% endif %} -{% endblock %} -{% block scripts %} - -{% endblock %} -HTMLEOF - - # ── settings.html ─────────────────────────────────────────── - cat > "${WEBGUI_DIR}/templates/settings.html" << 'HTMLEOF' -{% extends 'base.html' %} -{% block title %}Postavke{% endblock %} -{% block content %} -

Postavke

-
-
-
-
Promjena lozinke GUI-ja
-
-
-
-
-
- -
-
-
-
-
-
Informacije
-
-
-
Instalacijski dir
/opt/guacamole
-
Nginx konfig
/etc/nginx/.../guacamole
-
Basic Auth file
/etc/nginx/.guac_htpasswd
-
fail2ban jail
/etc/fail2ban/jail.d/guacamole.conf
-
-
-
-
-
-{% endblock %} -{% block scripts %} - -{% endblock %} -HTMLEOF + # ── Aplikacija ────────────────────────────────────────────── + # Izvor je webgui/ pokraj ove skripte. Ranije je install.sh nosio + # vlastitu inline kopiju koja se razisla s repoom — nedostajale su + # stranice Backup i Azuriranja koje README dokumentira. + if [[ ! -f "${SRC_DIR}/webgui/app.py" ]]; then + error "Nedostaje ${SRC_DIR}/webgui/ — kopirajte cijeli repo na server, ne samo install.sh" + fi + cp "${SRC_DIR}/webgui/app.py" "${WEBGUI_DIR}/app.py" + cp "${SRC_DIR}/webgui/templates/"*.html "${WEBGUI_DIR}/templates/" + info "Web GUI kopiran ($(find "${SRC_DIR}/webgui/templates" -name '*.html' | wc -l) predlozaka)" # ── config.json ───────────────────────────────────────────── cat > "${WEBGUI_DIR}/config.json" << EOF