From 84b62fc8841b9cf6d7e45a370872f27b56ab6632 Mon Sep 17 00:00:00 2001 From: Daan Selen Date: Mon, 16 Mar 2026 23:04:45 +0100 Subject: [PATCH] chore: add code update --- src/config.ini | 4 +- src/modules/routes/routes.py | 52 +++++--- src/modules/routes/routes_welcome.py | 38 +++++- src/modules/utilities/system_status.py | 164 +++++++++++++++++++++++++ src/modules/utilities/utilities.py | 22 +++- src/requirements.txt | 3 +- src/static/build-app.sh | 4 +- src/static/client/proxy.js | 1 + 8 files changed, 263 insertions(+), 25 deletions(-) create mode 100644 src/modules/utilities/system_status.py create mode 100644 src/static/client/proxy.js diff --git a/src/config.ini b/src/config.ini index 9527b6e..a656ebd 100644 --- a/src/config.ini +++ b/src/config.ini @@ -25,10 +25,10 @@ log_level = DEBUG [account] username = dselen -password = $2b$12$LId4p4WP3N0RLFi/TjNxseA1U.yybg2bJ8ufvcbpclUxckEX6OH.y +password = $2b$12$0dvZY7mNbEcjfJI09AqxbeKYhBwoNwBrD4OQNhlJ3HqXoAbZmhNFq enable_totp = False totp_verified = False -totp_key = NY7VFZFYIUM7FUZCDW7UMXQ7DMGGLSJU +totp_key = ON75CRNI7MGTA33PTHTQEZVXB5JKCEYK [other] welcome_session = False diff --git a/src/modules/routes/routes.py b/src/modules/routes/routes.py index fcae848..a806b00 100644 --- a/src/modules/routes/routes.py +++ b/src/modules/routes/routes.py @@ -17,6 +17,9 @@ from .locale import localeman from ..database.functions import functions from ..config.config import config +from ..utilities.utilities import utilities +from ..utilities.system_status import system_status + routes = flask.Blueprint("routes", __name__) white_list = [ @@ -158,19 +161,9 @@ def api_authenticate(): return make_resp_obj(False, error_msg, {"status": False}, 401) if totp_enabled: - return make_resp_obj( - False, - "Sorry, your username, password or OTP is incorrect.", - {}, - 401 - ) + return make_resp_obj(False, "Sorry, your username, password or OTP is incorrect.", {}, 401) else: - return make_resp_obj( - False, - "Sorry, your username or password is incorrect.", - {}, - 401 - ) + return make_resp_obj(False, "Sorry, your username or password is incorrect.", {}, 401) @routes.route('/') def index_handler(): @@ -216,6 +209,11 @@ def api_retrieve_dashboard_theme(): return make_resp_obj(True, "", config_server.get("wgdashboard_theme"), 200) +@routes.route('/api/getDashboardUpdate') +def api_retrieve_dashboard_update(): + utilities.update_available() + return make_resp_obj() + @routes.route('/api/getDashboardConfiguration') def api_retrieve_dashboard_config(): return make_resp_obj(data=flask.current_app.wgd_config) @@ -231,11 +229,31 @@ def api_totp_status(): return make_resp_obj(True, "", data, 200) +@routes.route('/api/getWireguardConfigurations') +def api_retrieve_wireguard_configurations(): + ok, config_server = config.filter(flask.current_app.wgd_config, 'SERVER') + if not ok: + log.error("failed to filter the config in-memory") + return make_resp_obj(False, 'Internal error', {}, 500) + + if "wg_conf_path" not in config_server: + return make_resp_obj(False, 'Internal error', {}, 500) + + wireguard_path = config_server.get('wg_conf_path', '/etc/wireguard') + if os.path.exists(wireguard_path): + present_confs = os.listdir(wireguard_path) + present_confs.sort() + + log.info(present_confs) + + return make_resp_obj(True, 'Wireguard', {}, 200) + +@routes.route('/api/systemStatus') +def api_system_status(): + status = system_status() + return make_resp_obj(True, "", status.to_json(), 200) + @routes.route('/health', methods=["GET"]) @routes.route('/healthz', methods=["GET"]) def health_handler(): - return make_resp_obj(True, - "Health Endpoint", - {"status": "ok"}, - 200 - ) \ No newline at end of file + return make_resp_obj(True, "Health Endpoint", {"status": "ok"}, 200) \ No newline at end of file diff --git a/src/modules/routes/routes_welcome.py b/src/modules/routes/routes_welcome.py index 9337424..c79f3a1 100644 --- a/src/modules/routes/routes_welcome.py +++ b/src/modules/routes/routes_welcome.py @@ -72,6 +72,42 @@ def api_welcome_get_totp(): log.error("failed to update the key in the configuration file") return make_resp_obj(False, 'Internal error', {}, 500) + # Very important to also refresh the config in-memory + ok, flask.current_app.wgd_config = config.read() + if not ok: + log.error("failed to refresh the in-memory configuration") + return make_resp_obj(False, 'Internal error', {}, 500) + return make_resp_obj(True, '', pyotp.totp.TOTP(totp_key).provisioning_uri(issuer_name="WGDashboard Admin")) - return make_resp_obj(False, 'Internal error', {}, 500) \ No newline at end of file + return make_resp_obj(False, 'Internal error', {}, 500) + +@routes_welcome.route('/api/Welcome_VerifyTotpLink', methods=["POST"]) +def api_welcome_verify_totp(): + ok, config_account = config.filter(flask.current_app.wgd_config, 'ACCOUNT') + if not ok: + log.error("failed to filter the config in-memory") + return make_resp_obj(False, 'Internal error', {}, 500) + + req_data = flask.request.get_json() + totp_code = pyotp.TOTP(config_account['totp_key'], interval=30).now() + + totp_match = totp_code == req_data['totp'] + if totp_match: + ok = config.update('ACCOUNT', 'totp_verified', True) + if not ok: + log.error("failed to update the key in the configuration file") + return make_resp_obj(False, 'Internal error', {}, 500) + + ok = config.update('ACCOUNT', 'enable_totp', True) + if not ok: + log.error("failed to update the key in the configuration file") + return make_resp_obj(False, 'Internal error', {}, 500) + + # Very important to also refresh the config in-memory + ok, flask.current_app.wgd_config = config.read() + if not ok: + log.error("failed to refresh the in-memory configuration") + return make_resp_obj(False, 'Internal error', {}, 500) + + return make_resp_obj(totp_match) \ No newline at end of file diff --git a/src/modules/utilities/system_status.py b/src/modules/utilities/system_status.py new file mode 100644 index 0000000..ff2b576 --- /dev/null +++ b/src/modules/utilities/system_status.py @@ -0,0 +1,164 @@ +#!/bin/env python3 + +import psutil +import shutil +import subprocess +import time +import flask + +import psutil, shutil, subprocess, time +from flask import current_app + + +class system_status: + def to_json(self): + return { + "CPU": self.get_cpu(), + "Memory": self.get_memory(), + "Disks": self.get_disks(), + "NetworkInterfaces": self.get_network(), + "NetworkInterfacesPriority": self.get_interface_priorities(), + "Processes": self.get_processes() + } + + def get_cpu(self): + try: + return { + "cpu_percent": psutil.cpu_percent(interval=1), + "cpu_percent_per_cpu": psutil.cpu_percent(interval=1, percpu=True) + } + except Exception as e: + current_app.logger.error("CPU error %s", e) + return {} + + def get_memory(self): + try: + v = psutil.virtual_memory() + s = psutil.swap_memory() + + return { + "VirtualMemory": { + "total": v.total, + "available": v.available, + "percent": v.percent + }, + "SwapMemory": { + "total": s.total, + "available": s.free, + "percent": s.percent + } + } + except Exception as e: + current_app.logger.error("Memory error %s", e) + return {} + + def get_disks(self): + disks = [] + try: + for p in psutil.disk_partitions(): + d = psutil.disk_usage(p.mountpoint) + + disks.append({ + "mountPoint": p.mountpoint, + "total": d.total, + "used": d.used, + "free": d.free, + "percent": d.percent + }) + except Exception as e: + current_app.logger.error("Disk error %s", e) + + return disks + + def get_network(self): + try: + first = psutil.net_io_counters(pernic=True) + time.sleep(1) + second = psutil.net_io_counters(pernic=True) + + result = {} + + for iface in first: + sent = (second[iface].bytes_sent - first[iface].bytes_sent) / 1024 / 1024 + recv = (second[iface].bytes_recv - first[iface].bytes_recv) / 1024 / 1024 + + result[iface] = { + **first[iface]._asdict(), + "realtime": { + "sent": round(sent, 4), + "recv": round(recv, 4) + } + } + + return result + + except Exception as e: + current_app.logger.error("Network error %s", e) + return {} + + def get_interface_priorities(self): + try: + if not shutil.which("ip"): + return {} + + result = subprocess.check_output(["ip", "route", "show"]).decode() + + priorities = {} + + for line in result.splitlines(): + if "metric" in line and "dev" in line: + parts = line.split() + dev = parts[parts.index("dev") + 1] + metric = int(parts[parts.index("metric") + 1]) + + priorities.setdefault(dev, metric) + + return priorities + + except Exception as e: + current_app.logger.error("Interface priority error %s", e) + return {} + + def get_processes(self): + cpu_list = [] + mem_list = [] + + try: + for proc in psutil.process_iter(): + + try: + name = proc.name() + cmd = " ".join(proc.cmdline()) + pid = proc.pid + + cpu = proc.cpu_percent() + mem = proc.memory_percent() + + cpu_list.append({ + "name": name, + "command": cmd, + "pid": pid, + "percent": cpu + }) + + mem_list.append({ + "name": name, + "command": cmd, + "pid": pid, + "percent": mem + }) + + except (psutil.NoSuchProcess, psutil.AccessDenied): + continue + + cpu_list.sort(key=lambda x: x["percent"], reverse=True) + mem_list.sort(key=lambda x: x["percent"], reverse=True) + + return { + "cpu_top_10": cpu_list[:20], + "memory_top_10": mem_list[:20] + } + + except Exception as e: + current_app.logger.error("Process error %s", e) + return {} \ No newline at end of file diff --git a/src/modules/utilities/utilities.py b/src/modules/utilities/utilities.py index 988221c..a2bf81e 100644 --- a/src/modules/utilities/utilities.py +++ b/src/modules/utilities/utilities.py @@ -3,6 +3,7 @@ import logging as log import os +import urllib.request class utilities(): @staticmethod @@ -19,5 +20,22 @@ class utilities(): return True except Exception as err: - log.critical('failed to create directory') - return False \ No newline at end of file + log.error('failed to create directory') + return False + + @staticmethod + def update_available() -> bool: + request = urllib.request.urlopen("https://api.github.com/repos/WGDashboard/WGDashboard/releases/latest", timeout=5).read() + + data = json.loads(request) + log.info(data) + + @staticmethod + def ProtocolsEnabled() -> list[str]: + from shutil import which + protocols = [] + if which('awg') is not None and which('awg-quick') is not None: + protocols.append("awg") + if which('wg') is not None and which('wg-quick') is not None: + protocols.append("wg") + return protocols \ No newline at end of file diff --git a/src/requirements.txt b/src/requirements.txt index aeed4ce..d4c7187 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -4,4 +4,5 @@ gunicorn==25.1.0 sqlalchemy==2.0.48 Flask==3.1.3 Werkzeug==3.1.6 -pyotp==2.9.0 \ No newline at end of file +pyotp==2.9.0 +psutil==7.2.2 \ No newline at end of file diff --git a/src/static/build-app.sh b/src/static/build-app.sh index 14f8208..0397ace 100755 --- a/src/static/build-app.sh +++ b/src/static/build-app.sh @@ -6,7 +6,7 @@ rm ./dist -rf echo "Compiling the new!" -cd ./admin && npm run build && cd .. -cd ./client && npm run build && cd .. +cd ./admin && npm install && npm run build && cd .. +cd ./client && npm install && npm run build && cd .. echo "Done!" diff --git a/src/static/client/proxy.js b/src/static/client/proxy.js new file mode 100644 index 0000000..c65fc32 --- /dev/null +++ b/src/static/client/proxy.js @@ -0,0 +1 @@ +export const proxy = "http://wg.local:10086/" \ No newline at end of file