diff --git a/README.md b/README.md
index 2c3a8c2..bc7a572 100644
--- a/README.md
+++ b/README.md
@@ -17,9 +17,12 @@
## 📣 What's New: v3.0
- 🎉 **New Features**
- - **Add Peers by Bulk: ** Now you can add peers by bulk, just simply set the amount and click add.
- - **Delete Peers by Bulk**: User can click the menu button (three-dots) on the bottom right of each configuration, and click **Delete Peers**, then select peers.
+ - **Moved from TinyDB to SQLite**: SQLite provide a better performance and loading speed when getting peers! Also avoided crashing the database due to **race condition**.
+ - **Add Peers by Bulk: ** User can add peers by bulk, just simply set the amount and click add.
+ - **Delete Peers by Bulk**: User can delete peers by bulk, without deleting peers one by one.
+ - **Download Peers in Zip**: User can download all *downloadable* peers in a zip.
- **Added Pre-shared Key to peers:** Now each peer can add with a pre-shared key to enhance security. Previously added peers can add the pre-shared key through the peer setting button.
+
- 🪚 **Bug Fixed**
- [IP Sorting range issues #99](https://github.com/donaldzou/WGDashboard/issues/99) [❤️ @barryboom]
- [INvalid character written to tunnel json file #108](https://github.com/donaldzou/WGDashboard/issues/108) [❤️ @ ikidd]
@@ -29,16 +32,12 @@
- **Key generating moved to front-end**: No longer need to use the server's WireGuard to generate keys, thanks to the `wireguard.js` from the [official repository](https://git.zx2c4.com/wireguard-tools/tree/contrib/keygen-html/wireguard.js)!
- **Peer transfer calculation**: each peer will now show all transfer amount (previously was only showing transfer amount from the last configuration start-up).
- **UI adjustment on running peers**: peers will have a new style indicating that it is running.
- - **Moved from TinyDB to SQLite**: This could provide better performance and loading speed when showing peers, also avoided changing the database could crash.
- - UI adjustment on the whole dashboard.
- **`wgd.sh` finally can update itself**: So now user could update the whole dashboard from `wgd.sh`, with the `update` command.
-
+*And many other small changes for performance and bug fixes! :laughing:*
-
-
## Table of Content
- [💡 Features](#-features)
@@ -99,11 +98,13 @@
- Python 3.7+ & Pip3
+- Browser support CSS3 and ES6
+
## 🛠 Install
1. Download WGDashboard
```shell
- git clone -b v2.3.1 https://github.com/donaldzou/WGDashboard.git wgdashboard
+ git clone -b v3.0 https://github.com/donaldzou/WGDashboard.git wgdashboard
2. Open the WGDashboard folder
@@ -276,22 +277,30 @@ In the `src` folder, it contained a file called `wg-dashboard.service`, we can u
#### Dashboard Configuration file
-Since version 2.0, WGDashboard will be using a configuration file called `wg-dashboard.ini`, (It will generate automatically after first time running the dashboard). More options will include in future versions, and for now it included the following config:
+Since version 2.0, WGDashboard will be using a configuration file called `wg-dashboard.ini`, (It will generate automatically after first time running the dashboard). More options will include in future versions, and for now it included the following configurations:
-| | Description | Default | Available in Setting |
-| --------------- | ------------------------------------------------------------ | ------------------------ | -------------------- |
-| **`[Account]`** | | | |
-| `username` | Dashboard login username | `admin` | Yes |
-| `password` | Password, will be hash with SHA256 | `admin` hashed in SHA256 | Yes |
-| **`[Server]`** | | | |
-| `wg_conf_path` | The path of all the Wireguard configurations | `/etc/wireguard` | Yes |
-| `app_ip` | IP address the dashboard will run with | `0.0.0.0` | Yes |
-| `app_port` | Port the the dashboard will run with | `10086` | Yes |
-| `auth_req` | Does the dashboard need authentication to access | `true` | No |
-| | If `auth_req = false` , user will not be access the **Setting** tab due to security consideration. **User can only edit the file directly in system**. | | |
-| `version` | Dashboard Version | `v2.2` | No |
-
-**Except `auth_req` due to security consideration.**
+| | Description | Default | Edit Available |
+| ---------------------------- | ------------------------------------------------------------ | ---------------------------------------------------- | -------------- |
+| **`[Account]`** | *Configuration on account* | | |
+| `username` | Dashboard login username | `admin` | Yes |
+| `password` | Password, will be hash with SHA256 | `admin` hashed in SHA256 | Yes |
+| | | | |
+| **`[Server]`** | *Configuration on dashboard* | | |
+| `wg_conf_path` | The path of all the Wireguard configurations | `/etc/wireguard` | Yes |
+| `app_ip` | IP address the dashboard will run with | `0.0.0.0` | Yes |
+| `app_port` | Port the the dashboard will run with | `10086` | Yes |
+| `auth_req` | Does the dashboard need authentication to access, if `auth_req = false` , user will not be access the **Setting** tab due to security consideration. **User can only edit the file directly in system**. | `true` | **No** |
+| `version` | Dashboard Version | `v3.0` | **No** |
+| `dashboard_refresh_interval` | How frequent the dashboard will refresh on the configuration page | `60000ms` | Yes |
+| `dashboard_sort` | How configuration is sorting | `status` | Yes |
+| | | | |
+| **`[Peers]`** | *Default Settings on a new peer* | | |
+| `peer_global_dns` | DNS Server | `1.1.1.1` | Yes |
+| `peer_endpoint_allowed_ip` | Endpoint Allowed IP | `0.0.0.0/0` | Yes |
+| `peer_display_mode` | How peer will display | `grid` | Yes |
+| `remote_endpoint` | Remote Endpoint (i.e where your peers will connect to) | *depends on your server's default network interface* | Yes |
+| `peer_mtu` | Maximum Transmit Unit | `1420` | |
+| `peer_keep_alive` | Keep Alive | `21` | Yes |
#### Generating QR code and peer configuration file (.conf)
@@ -322,14 +331,18 @@ Endpoint = 0.0.0.0:51820
## ❓ How to update the dashboard?
-1. Change your directory to `wireguard-dashboard`
+1. Change your directory to `wgdashboard`
```shell
- cd wireguard-dashboard/src
+ cd wgdashboard
```
+
2. Update the dashboard
```shell
- sudo ./wgd.sh update
+ git pull https://github.com/donaldzou/WGDashboard.git v3.0 --force
```
+
+Starting with `v3.0`, you can simply do `./wgd.sh update` !! (I hope, lol)
+
## 🔍 Screenshot
![Sign In Page](img/SignIn.png)
@@ -342,7 +355,7 @@ Endpoint = 0.0.0.0:51820
![Edit Peer](img/EditPeer.png)
-![Delete Peer](img/DeletePeer.png)
+![Delete Peer](img/DeleteBulk.png)
![Dashboard Setting](img/DashboardSetting.png)
@@ -350,8 +363,6 @@ Endpoint = 0.0.0.0:51820
![Traceroute](img/Traceroute.png)
-
-
## ⏰ Changelog
#### v2.3.1 - Sep 8, 2021
diff --git a/img/AddPeer.png b/img/AddPeer.png
index e130b01..d4627ce 100644
Binary files a/img/AddPeer.png and b/img/AddPeer.png differ
diff --git a/img/Configuration.png b/img/Configuration.png
index 393c4bf..317a68b 100644
Binary files a/img/Configuration.png and b/img/Configuration.png differ
diff --git a/img/DashboardSetting.png b/img/DashboardSetting.png
index b09ced3..463f947 100644
Binary files a/img/DashboardSetting.png and b/img/DashboardSetting.png differ
diff --git a/img/DeleteBulk.png b/img/DeleteBulk.png
new file mode 100644
index 0000000..afa2f53
Binary files /dev/null and b/img/DeleteBulk.png differ
diff --git a/img/EditPeer.png b/img/EditPeer.png
index 9bb9858..fed3bb3 100644
Binary files a/img/EditPeer.png and b/img/EditPeer.png differ
diff --git a/img/HomePage.png b/img/HomePage.png
index 41c64dc..2eab836 100644
Binary files a/img/HomePage.png and b/img/HomePage.png differ
diff --git a/img/QRCode.png b/img/QRCode.png
index 004e27b..f2d8b6d 100644
Binary files a/img/QRCode.png and b/img/QRCode.png differ
diff --git a/img/SearchIP.png b/img/SearchIP.png
new file mode 100644
index 0000000..0c78d65
Binary files /dev/null and b/img/SearchIP.png differ
diff --git a/src/dashboard.py b/src/dashboard.py
index f63a14c..0cbddf9 100644
--- a/src/dashboard.py
+++ b/src/dashboard.py
@@ -3,7 +3,6 @@
Under Apache-2.0 License
"""
-# TODO: Testing migrate to sqlite
import sqlite3
from flask import g
import configparser
@@ -32,7 +31,7 @@ from util import regex_match, check_DNS, check_Allowed_IPs, check_remote_endpoin
# Dashboard Version
DASHBOARD_VERSION = 'v3.0'
-# WireGuard configuration path
+# WireGuard's configuration path
WG_CONF_PATH = None
# Dashboard Config Name
configuration_path = os.getenv('CONFIGURATION_PATH', '.')
@@ -51,18 +50,20 @@ app.config['TEMPLATES_AUTO_RELOAD'] = True
QRcode(app)
+# TODO: use class and object oriented programming
+
def connect_db():
+ """
+ Connect to the database
+ @return: sqlite3.Connection
+ """
return sqlite3.connect(os.path.join(configuration_path, 'db', 'wgdashboard.db'))
-# TODO use class and object oriented programming
-
-# Read / Write Dashboard Config File
def get_dashboard_conf():
- """Dashboard Configuration Related
-
- :return: A config parser object
- :rtype: configparser.ConfigParser
+ """
+ Get dashboard configuration
+ @return: configparser.ConfigParser
"""
config = configparser.ConfigParser(strict=False)
@@ -71,10 +72,9 @@ def get_dashboard_conf():
def set_dashboard_conf(config):
- """Configuration writer
-
- :param config: A config parser object
- :type config: configparser.ConfigParser
+ """
+ Write to configuration
+ @param config: Input configuration
"""
with open(DASHBOARD_CONF, "w", encoding='utf-8') as conf_object:
@@ -83,17 +83,17 @@ def set_dashboard_conf(config):
# Get all keys from a configuration
def get_conf_peer_key(config_name):
- """Get the peers keys of wireguard interface.
-
- :param config_name: Name of WG interface
- :type config_name: str
- :return: Return list of peers keys or text if configuration not running
- :rtype: list, str
+ """
+ Get the peers keys of wireguard interface.
+ @param config_name: Name of WG interface
+ @type config_name: str
+ @return: Return list of peers keys or text if configuration not running
+ @rtype: list, str
"""
try:
- peers_keys = subprocess.run(f"wg show {config_name} peers",
- check=True, shell=True, capture_output=True).stdout
+ peers_keys = subprocess.check_output(f"wg show {config_name} peers",
+ shell=True, stderr=subprocess.STDOUT)
peers_keys = peers_keys.decode("UTF-8").split()
return peers_keys
except subprocess.CalledProcessError:
@@ -102,19 +102,19 @@ def get_conf_peer_key(config_name):
# Get numbers of connected peer of a configuration
def get_conf_running_peer_number(config_name):
- """Get number of running peers on wireguard interface.
-
- :param config_name: Name of WG interface
- :type config_name: str
- :return: Number of running peers, or test if configuration not running
- :rtype: int, str
+ """
+ Get number of running peers on wireguard interface.
+ @param config_name: Name of WG interface
+ @type config_name: str
+ @return: Number of running peers, or test if configuration not running
+ @rtype: int, str
"""
running = 0
# Get latest handshakes
try:
- data_usage = subprocess.run(f"wg show {config_name} latest-handshakes",
- check=True, shell=True, capture_output=True).stdout
+ data_usage = subprocess.check_output(f"wg show {config_name} latest-handshakes",
+ shell=True, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError:
return "stopped"
data_usage = data_usage.decode("UTF-8").split()
@@ -132,12 +132,12 @@ def get_conf_running_peer_number(config_name):
# TODO use modules for working with ini(configparser or wireguard)
# Read [Interface] section from configuration file
def read_conf_file_interface(config_name):
- """Get interface settings.
-
- :param config_name: Name of WG interface
- :type config_name: str
- :return: Dictionary with interface settings
- :rtype: dict
+ """
+ Get interface settings.
+ @param config_name: Name of WG interface
+ @type config_name: str
+ @return: Dictionary with interface settings
+ @rtype: dict
"""
conf_location = WG_CONF_PATH + "/" + config_name + ".conf"
@@ -155,14 +155,16 @@ def read_conf_file_interface(config_name):
# TODO use modules for working with ini(configparser or wireguard)
-# Read the whole configuration file
-def read_conf_file(config_name):
- """Get configurations from file of wireguard interface.
+# Tried to use configparser but it does not support sections with the same name
- :param config_name: Name of WG interface
- :type config_name: str
- :return: Dictionary with interface and peers settings
- :rtype: dict
+
+def read_conf_file(config_name):
+ """
+ Get configurations from file of wireguard interface.
+ @param config_name: Name of WG interface
+ @type config_name: str
+ @return: Dictionary with interface and peers settings
+ @rtype: dict
"""
# Read Configuration File Start
@@ -203,12 +205,17 @@ def read_conf_file(config_name):
return conf_peer_data
-# Get the latest handshake from all peers of a configuration
def get_latest_handshake(config_name):
+ """
+ Get the latest handshake from all peers of a configuration
+ @param config_name: Configuration name
+ @return: str
+ """
+
# Get latest handshakes
try:
- data_usage = subprocess.run(f"wg show {config_name} latest-handshakes",
- check=True, shell=True, capture_output=True).stdout
+ data_usage = subprocess.check_output(f"wg show {config_name} latest-handshakes",
+ shell=True, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError:
return "stopped"
data_usage = data_usage.decode("UTF-8").split()
@@ -228,15 +235,18 @@ def get_latest_handshake(config_name):
g.cur.execute("UPDATE %s SET latest_handshake = '(None)', status = '%s' WHERE id='%s'"
% (config_name, status, data_usage[count]))
count += 2
- return None
-# Get transfer from all peers of a configuration
def get_transfer(config_name):
+ """
+ Get transfer from all peers of a configuration
+ @param config_name: Configuration name
+ @return: str
+ """
# Get transfer
try:
- data_usage = subprocess.run(f"wg show {config_name} transfer",
- check=True, shell=True, capture_output=True).stdout
+ data_usage = subprocess.check_output(f"wg show {config_name} transfer",
+ shell=True, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError:
return "stopped"
data_usage = data_usage.decode("UTF-8").split("\n")
@@ -268,15 +278,18 @@ def get_transfer(config_name):
g.cur.execute("UPDATE %s SET total_receive = %f, total_sent = %f, total_data = %f WHERE id = '%s'" %
(config_name, round(total_receive, 4), round(total_sent, 4),
round(total_receive + total_sent, 4), data_usage[i][0]))
- return None
-# Get endpoint from all peers of a configuration
def get_endpoint(config_name):
+ """
+ Get endpoint from all peers of a configuration
+ @param config_name: Configuration name
+ @return: str
+ """
# Get endpoint
try:
- data_usage = subprocess.run(f"wg show {config_name} endpoints",
- check=True, shell=True, capture_output=True).stdout
+ data_usage = subprocess.check_output(f"wg show {config_name} endpoints",
+ shell=True, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError:
return "stopped"
data_usage = data_usage.decode("UTF-8").split()
@@ -285,19 +298,27 @@ def get_endpoint(config_name):
g.cur.execute("UPDATE " + config_name + " SET endpoint = '%s' WHERE id = '%s'"
% (data_usage[count + 1], data_usage[count]))
count += 2
- return None
-# Get allowed ips from all peers of a configuration
def get_allowed_ip(conf_peer_data, config_name):
+ """
+ Get allowed ips from all peers of a configuration
+ @param conf_peer_data: Configuration peer data
+ @param config_name: Configuration name
+ @return: None
+ """
# Get allowed ip
for i in conf_peer_data["Peers"]:
g.cur.execute("UPDATE " + config_name + " SET allowed_ip = '%s' WHERE id = '%s'"
% (i.get('AllowedIPs', '(None)'), i["PublicKey"]))
-# Look for new peers from WireGuard
def get_all_peers_data(config_name):
+ """
+ Look for new peers from WireGuard
+ @param config_name: Configuration name
+ @return: None
+ """
conf_peer_data = read_conf_file(config_name)
config = get_dashboard_conf()
failed_index = []
@@ -340,7 +361,6 @@ def get_all_peers_data(config_name):
else:
print("Trying to parse a peer doesn't have public key...")
failed_index.append(i)
-
for i in failed_index:
conf_peer_data['Peers'].pop(i)
# Remove peers no longer exist in WireGuard configuration file
@@ -349,26 +369,23 @@ def get_all_peers_data(config_name):
for i in db_key:
if i not in wg_key:
g.cur.execute("DELETE FROM %s WHERE id = '%s'" % (config_name, i))
- tic = time.perf_counter()
get_latest_handshake(config_name)
get_transfer(config_name)
get_endpoint(config_name)
get_allowed_ip(conf_peer_data, config_name)
- toc = time.perf_counter()
- print(f"Finish fetching data in {toc - tic:0.4f} seconds")
# Search for peers
def get_peers(config_name, search, sort_t):
- """Get all peers.
-
- :param config_name: Name of WG interface
- :type config_name: str
- :param search: Search string
- :type search: str
- :param sort_t: TODO
- :type sort_t: str
- :return: TODO
+ """
+ Get all peers.
+ @param config_name: Name of WG interface
+ @type config_name: str
+ @param search: Search string
+ @type search: str
+ @param sort_t: Sorting tag
+ @type sort_t: str
+ @return: list
"""
tic = time.perf_counter()
col = g.cur.execute("PRAGMA table_info(" + config_name + ")").fetchall()
@@ -391,21 +408,20 @@ def get_peers(config_name, search, sort_t):
return result
-# Get configuration public key
def get_conf_pub_key(config_name):
- """Get public key for configuration.
-
- :param config_name: Name of WG interface
- :type config_name: str
- :return: Return public key or empty string
- :rtype: str
+ """
+ Get public key for configuration.
+ @param config_name: Name of WG interface
+ @type config_name: str
+ @return: Return public key or empty string
+ @rtype: str
"""
try:
conf = configparser.ConfigParser(strict=False)
conf.read(WG_CONF_PATH + "/" + config_name + ".conf")
pri = conf.get("Interface", "PrivateKey")
- pub = subprocess.run(f"echo '{pri}' | wg pubkey", check=True, shell=True, capture_output=True).stdout
+ pub = subprocess.check_output(f"echo '{pri}' | wg pubkey", shell=True, stderr=subprocess.STDOUT)
conf.clear()
return pub.decode().strip("\n")
except configparser.NoSectionError:
@@ -414,12 +430,12 @@ def get_conf_pub_key(config_name):
# Get configuration listen port
def get_conf_listen_port(config_name):
- """Get listen port number.
-
- :param config_name: Name of WG interface
- :type config_name: str
- :return: Return number of port or empty string
- :rtype: str
+ """
+ Get listen port number.
+ @param config_name: Name of WG interface
+ @type config_name: str
+ @return: Return number of port or empty string
+ @rtype: str
"""
conf = configparser.ConfigParser(strict=False)
@@ -429,8 +445,8 @@ def get_conf_listen_port(config_name):
port = conf.get("Interface", "ListenPort")
except (configparser.NoSectionError, configparser.NoOptionError):
if get_conf_status(config_name) == "running":
- port = subprocess.run(f"wg show {config_name} listen-port",
- check=True, shell=True, capture_output=True).stdout
+ port = subprocess.check_output(f"wg show {config_name} listen-port",
+ shell=True, stderr=subprocess.STDOUT)
port = port.decode("UTF-8")
conf.clear()
return port
@@ -529,18 +545,17 @@ def gen_public_key(private_key):
return {"status": 'failed', "msg": "Key is not the correct length or format", "data": ""}
-# Check if private key and public key match
def f_check_key_match(private_key, public_key, config_name):
- """TODO
-
- :param private_key: Private key
- :type private_key: str
- :param public_key: Public key
- :type public_key: str
- :param config_name: Name of WG interface
- :type config_name: str
- :return: Return dictionary with status
- :rtype: dict
+ """
+ Check if private key and public key match
+ @param private_key: Private key
+ @type private_key: str
+ @param public_key: Public key
+ @type public_key: str
+ @param config_name: Name of WG interface
+ @type config_name: str
+ @return: Return dictionary with status
+ @rtype: dict
"""
result = gen_public_key(private_key)
@@ -581,6 +596,7 @@ def f_available_ips(config_name):
existed.append(ipaddress.ip_address(add))
peers = g.cur.execute("SELECT allowed_ip FROM " + config_name).fetchall()
for i in peers:
+ print(i[0])
add = i[0].split(",")
for k in add:
a, s = k.split("/")
@@ -706,7 +722,12 @@ def index():
:return: TODO
:rtype: TODO
"""
- return render_template('index.html', conf=get_conf_list())
+ msg = ""
+ if "switch_msg" in session:
+ msg = session["switch_msg"]
+ session.pop("switch_msg")
+
+ return render_template('index.html', conf=get_conf_list(), msg=msg)
# Setting Page
@@ -1026,7 +1047,6 @@ def get_conf(config_name):
return jsonify(conf_data)
-
# Turn on / off a configuration
@app.route('/switch/', methods=['GET'])
def switch(config_name):
@@ -1038,21 +1058,20 @@ def switch(config_name):
:rtype: TODO
"""
- if "username" not in session:
- print("User not logged in")
- return redirect(url_for("signin"))
status = get_conf_status(config_name)
if status == "running":
try:
- subprocess.run("wg-quick down " + config_name,
- check=True, shell=True, capture_output=True).stdout
- except subprocess.CalledProcessError:
+ check = subprocess.check_output("wg-quick down " + config_name,
+ shell=True, stderr=subprocess.STDOUT)
+ except subprocess.CalledProcessError as exc:
+ session["switch_msg"] = exc.output.strip().decode("utf-8")
return redirect('/')
elif status == "stopped":
try:
- subprocess.run("wg-quick up " + config_name,
- check=True, shell=True, capture_output=True).stdout
- except subprocess.CalledProcessError:
+ subprocess.check_output("wg-quick up " + config_name,
+ shell=True, stderr=subprocess.STDOUT)
+ except subprocess.CalledProcessError as exc:
+ session["switch_msg"] = exc.output.strip().decode("utf-8")
return redirect('/')
return redirect(request.referrer)
@@ -1252,16 +1271,16 @@ def save_peer_setting(config_name):
tmp_psk = open("tmp_edit_psk.txt", "w+")
tmp_psk.write(preshared_key)
tmp_psk.close()
- change_psk = subprocess.run(f"wg set {config_name} peer {id} preshared-key tmp_edit_psk.txt",
- shell=True, check=True, stderr=subprocess.STDOUT)
+ change_psk = subprocess.check_output(f"wg set {config_name} peer {id} preshared-key tmp_edit_psk.txt",
+ shell=True, stderr=subprocess.STDOUT)
if change_psk.decode("UTF-8") != "":
return jsonify({"status": "failed", "msg": change_psk.decode("UTF-8")})
if allowed_ip == "":
allowed_ip = '""'
allowed_ip = allowed_ip.replace(" ", "")
- change_ip = subprocess.run(f"wg set {config_name} peer {id} allowed-ips {allowed_ip}",
- shell=True, check=True, stderr=subprocess.STDOUT)
- subprocess.run(f'wg-quick save {config_name}', shell=True, check=True, stderr=subprocess.STDOUT)
+ change_ip = subprocess.check_output(f"wg set {config_name} peer {id} allowed-ips {allowed_ip}",
+ shell=True, stderr=subprocess.STDOUT)
+ subprocess.check_output(f'wg-quick save {config_name}', shell=True, stderr=subprocess.STDOUT)
if change_ip.decode("UTF-8") != "":
return jsonify({"status": "failed", "msg": change_ip.decode("UTF-8")})
sql = "UPDATE " + config_name + " SET name = ?, private_key = ?, DNS = ?, endpoint_allowed_ip = ?, mtu = ?, keepalive = ?, preshared_key = ? WHERE id = ?"
@@ -1600,7 +1619,6 @@ def init_dashboard():
config['Server'] = {}
if 'wg_conf_path' not in config['Server']:
config['Server']['wg_conf_path'] = '/etc/wireguard'
- # TODO: IPv6 for the app IP might need to configure with Gunicorn...
if 'app_ip' not in config['Server']:
config['Server']['app_ip'] = '0.0.0.0'
if 'app_port' not in config['Server']:
diff --git a/src/static/.DS_Store b/src/static/.DS_Store
index ba3b77f..d127be4 100644
Binary files a/src/static/.DS_Store and b/src/static/.DS_Store differ
diff --git a/src/static/css/dashboard.css b/src/static/css/dashboard.css
index 6399318..1c33bc5 100644
--- a/src/static/css/dashboard.css
+++ b/src/static/css/dashboard.css
@@ -502,4 +502,14 @@ main{
.no-responding{
transition: all 1s ease-in-out;
filter: blur(10px);
+}
+
+pre.index-alert{
+ margin-bottom: 0;
+ padding: 1rem;
+ background-color: #343a40;
+ border: 1px solid rgba(0,0,0,.125);
+ border-radius: .25rem;
+ margin-top: 1rem;
+ color: white;
}
\ No newline at end of file
diff --git a/src/static/css/dashboard.min.css b/src/static/css/dashboard.min.css
index 4204338..5f39e1a 100644
--- a/src/static/css/dashboard.min.css
+++ b/src/static/css/dashboard.min.css
@@ -1 +1 @@
-@-webkit-keyframes rotating{0%{-webkit-transform:rotate(0deg);-o-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(360deg);-o-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes rotating{0%{-ms-transform:rotate(0deg);-moz-transform:rotate(0deg);-webkit-transform:rotate(0deg);-o-transform:rotate(0deg);transform:rotate(0deg)}to{-ms-transform:rotate(360deg);-moz-transform:rotate(360deg);-webkit-transform:rotate(360deg);-o-transform:rotate(360deg);transform:rotate(360deg)}}@-webkit-keyframes loading{0%,to{background-color:#dfdfdf}50%{background-color:#adadad}}@-moz-keyframes loading{0%,to{background-color:#dfdfdf}50%{background-color:#adadad}}body{font-size:.875rem}.feather{width:16px;height:16px;vertical-align:text-bottom}.sidebar{position:fixed;top:0;bottom:0;left:0;z-index:100;padding:48px 0 0;box-shadow:inset -1px 0 0 rgba(0,0,0,.1)}.sidebar-sticky{position:relative;top:0;height:calc(100vh - 48px);padding-top:.5rem;overflow-x:hidden;overflow-y:auto}@supports ((position:-webkit-sticky) or (position:sticky)){.sidebar-sticky{position:-webkit-sticky;position:sticky}}.sidebar .nav-link{font-weight:500;color:#333;transition:.2s cubic-bezier(.82,-.07,0,1.01)}.nav-link:hover{padding-left:30px}.sidebar .nav-link .feather{margin-right:4px;color:#999}.sidebar .nav-link.active{color:#007bff}.sidebar .nav-link.active .feather,.sidebar .nav-link:hover .feather{color:inherit}.sidebar-heading{font-size:.75rem;text-transform:uppercase}.navbar-brand{padding-top:.75rem;padding-bottom:.75rem;font-size:1rem;background-color:rgba(0,0,0,.25);box-shadow:inset -1px 0 0 rgba(0,0,0,.25)}.navbar .navbar-toggler{top:.25rem;right:1rem}.form-control{transition:all .2s ease-in-out}.form-control:disabled{cursor:not-allowed}.navbar .form-control{padding:.75rem 1rem;border-width:0;border-radius:0}.form-control-dark{color:#fff;background-color:rgba(255,255,255,.1);border-color:rgba(255,255,255,.1)}.form-control-dark:focus{border-color:transparent;box-shadow:0 0 0 3px rgba(255,255,255,.25)}.dot{width:10px;height:10px;border-radius:50px;display:inline-block;margin-left:10px}.dot-running{background-color:#28a745!important;box-shadow:0 0 0 .2rem #28a74545}.h6-dot-running{margin-left:.3rem}.dot-stopped{background-color:#6c757d!important}.card-running{border-color:#28a745}.info h6{line-break:anywhere;transition:.2s ease-in-out}.info .row .col-sm{display:flex;flex-direction:column}.info .row .col-sm small{display:flex}.info .row .col-sm small strong:last-child(1){margin-left:auto!important}.btn-control{border:0!important;padding:0 1rem 0 0}.btn-control:active,.btn-control:focus{background-color:transparent!important;border:0!important;box-shadow:none}.share_peer_btn_group .btn-control{padding:0 0 0 1rem}.btn-control:hover{background:#fff}.btn-delete-peer:hover{color:#dc3545}.btn-setting-peer:hover{color:#007bff}.btn-download-peer:hover{color:#17a2b8}.login-container{padding:2rem}@media (max-width:992px){.card-col{margin-bottom:1rem}}.switch{font-size:2rem}.switch:hover{text-decoration:none}.btn-group-label:hover{color:#007bff;border-color:#007bff;background:#fff}@media (max-width:768px){.peer_data_group{text-align:left}}.index-switch{text-align:right}main{margin-bottom:3rem}.peer_list{margin-bottom:7rem}@media (max-width:768px){.add_btn{bottom:1.5rem!important}.peer_list{margin-bottom:7rem!important}}.btn-manage-group{z-index:99;position:fixed;bottom:3rem;right:2rem;display:flex}.btn-manage-group .setting_btn_menu{position:absolute;top:-124px;background-color:#fff;padding:1rem 0;right:0;box-shadow:0 10px 20px rgb(0 0 0/19%),0 6px 6px rgb(0 0 0/23%);border-radius:10px;min-width:200px;display:none;transform:translateY(-30px);opacity:0;transition:all .3s cubic-bezier(.58,.03,.05,1.28)}.btn-manage-group .setting_btn_menu.show{display:block}.setting_btn_menu.showing{transform:translateY(0);opacity:1}.setting_btn_menu a{display:flex;padding:.5rem 1rem;transition:all .1s ease-in-out;font-size:1rem;align-items:center;cursor:pointer}.setting_btn_menu a:hover{background-color:#efefef;text-decoration:none}.setting_btn_menu a i{margin-right:auto!important}.add_btn{margin-right:1rem}.add_btn,.setting_btn{height:54px;z-index:99;border-radius:100px!important;padding:0 14px;box-shadow:0 10px 20px rgba(0,0,0,.19),0 6px 6px rgba(0,0,0,.23);font-size:1.5rem}.rotating::before{-webkit-animation:rotating .75s linear infinite;-moz-animation:rotating .75s linear infinite;-ms-animation:rotating .75s linear infinite;-o-animation:rotating .75s linear infinite;animation:rotating .75s linear infinite}.peer_private_key_textbox_switch{position:absolute;right:2rem;transform:translateY(-28px);font-size:1.2rem;cursor:pointer}#peer_private_key_textbox,#private_key,#public_key{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}.progress-bar{transition:.3s ease-in-out}.key{transition:.2s ease-in-out;cursor:pointer}.key:hover{color:#007bff}.card,.form-control{border-radius:10px}.peer_list .card .button-group{height:22px}.btn{border-radius:8px}.modal-content{border-radius:10px}.tooltip-inner{font-size:.8rem}#conf_status_btn,.conf_card{transition:.2s ease-in-out}.conf_card:hover{border-color:#007bff;cursor:pointer}.info_loading{animation:loading 2s infinite ease-in-out;border-radius:5px;height:19px;transition:.3s ease-in-out}#conf_status_btn.info_loading{height:38px;border-radius:5px;animation:loading 3s infinite ease-in-out}#qrcode_img img{width:100%}#selected_ip_list .badge,#selected_peer_list .badge{margin:.1rem}#add_modal.ip_modal_open{transition:filter .2s ease-in-out;filter:brightness(.5)}#delete_bulk_modal .list-group a.active{background-color:#dc3545;border-color:#dc3545}#selected_peer_list{max-height:80px;overflow-y:scroll;overflow-x:hidden}.no-responding,.no-response{transition:all 1s ease-in-out}.no-response{width:100%;height:100%;position:fixed;background:#000000ba;z-index:10000;display:none;flex-direction:column;align-items:center;justify-content:center;opacity:0}.no-response.active{display:flex}.no-response.active.show{opacity:100}.no-response .container>*{text-align:center}.no-responding{filter:blur(10px)}
\ No newline at end of file
+@-webkit-keyframes rotating{0%{-webkit-transform:rotate(0deg);-o-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(360deg);-o-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes rotating{0%{-ms-transform:rotate(0deg);-moz-transform:rotate(0deg);-webkit-transform:rotate(0deg);-o-transform:rotate(0deg);transform:rotate(0deg)}to{-ms-transform:rotate(360deg);-moz-transform:rotate(360deg);-webkit-transform:rotate(360deg);-o-transform:rotate(360deg);transform:rotate(360deg)}}@-webkit-keyframes loading{0%,to{background-color:#dfdfdf}50%{background-color:#adadad}}@-moz-keyframes loading{0%,to{background-color:#dfdfdf}50%{background-color:#adadad}}body{font-size:.875rem}.feather{width:16px;height:16px;vertical-align:text-bottom}.sidebar{position:fixed;top:0;bottom:0;left:0;z-index:100;padding:48px 0 0;box-shadow:inset -1px 0 0 rgba(0,0,0,.1)}.sidebar-sticky{position:relative;top:0;height:calc(100vh - 48px);padding-top:.5rem;overflow-x:hidden;overflow-y:auto}@supports ((position:-webkit-sticky) or (position:sticky)){.sidebar-sticky{position:-webkit-sticky;position:sticky}}.sidebar .nav-link{font-weight:500;color:#333;transition:.2s cubic-bezier(.82,-.07,0,1.01)}.nav-link:hover{padding-left:30px}.sidebar .nav-link .feather{margin-right:4px;color:#999}.sidebar .nav-link.active{color:#007bff}.sidebar .nav-link.active .feather,.sidebar .nav-link:hover .feather{color:inherit}.sidebar-heading{font-size:.75rem;text-transform:uppercase}.navbar-brand{padding-top:.75rem;padding-bottom:.75rem;font-size:1rem;background-color:rgba(0,0,0,.25);box-shadow:inset -1px 0 0 rgba(0,0,0,.25)}.navbar .navbar-toggler{top:.25rem;right:1rem}.form-control{transition:all .2s ease-in-out}.form-control:disabled{cursor:not-allowed}.navbar .form-control{padding:.75rem 1rem;border-width:0;border-radius:0}.form-control-dark{color:#fff;background-color:rgba(255,255,255,.1);border-color:rgba(255,255,255,.1)}.form-control-dark:focus{border-color:transparent;box-shadow:0 0 0 3px rgba(255,255,255,.25)}.dot{width:10px;height:10px;border-radius:50px;display:inline-block;margin-left:10px}.dot-running{background-color:#28a745!important;box-shadow:0 0 0 .2rem #28a74545}.h6-dot-running{margin-left:.3rem}.dot-stopped{background-color:#6c757d!important}.card-running{border-color:#28a745}.info h6{line-break:anywhere;transition:.2s ease-in-out}.info .row .col-sm{display:flex;flex-direction:column}.info .row .col-sm small{display:flex}.info .row .col-sm small strong:last-child(1){margin-left:auto!important}.btn-control{border:0!important;padding:0 1rem 0 0}.btn-control:active,.btn-control:focus{background-color:transparent!important;border:0!important;box-shadow:none}.share_peer_btn_group .btn-control{padding:0 0 0 1rem}.btn-control:hover{background:#fff}.btn-delete-peer:hover{color:#dc3545}.btn-setting-peer:hover{color:#007bff}.btn-download-peer:hover{color:#17a2b8}.login-container{padding:2rem}@media (max-width:992px){.card-col{margin-bottom:1rem}}.switch{font-size:2rem}.switch:hover{text-decoration:none}.btn-group-label:hover{color:#007bff;border-color:#007bff;background:#fff}@media (max-width:768px){.peer_data_group{text-align:left}}.index-switch{text-align:right}main{margin-bottom:3rem}.peer_list{margin-bottom:7rem}@media (max-width:768px){.add_btn{bottom:1.5rem!important}.peer_list{margin-bottom:7rem!important}}.btn-manage-group{z-index:99;position:fixed;bottom:3rem;right:2rem;display:flex}.btn-manage-group .setting_btn_menu{position:absolute;top:-124px;background-color:#fff;padding:1rem 0;right:0;box-shadow:0 10px 20px rgb(0 0 0/19%),0 6px 6px rgb(0 0 0/23%);border-radius:10px;min-width:200px;display:none;transform:translateY(-30px);opacity:0;transition:all .3s cubic-bezier(.58,.03,.05,1.28)}.btn-manage-group .setting_btn_menu.show{display:block}.setting_btn_menu.showing{transform:translateY(0);opacity:1}.setting_btn_menu a{display:flex;padding:.5rem 1rem;transition:all .1s ease-in-out;font-size:1rem;align-items:center;cursor:pointer}.setting_btn_menu a:hover{background-color:#efefef;text-decoration:none}.setting_btn_menu a i{margin-right:auto!important}.add_btn{margin-right:1rem}.add_btn,.setting_btn{height:54px;z-index:99;border-radius:100px!important;padding:0 14px;box-shadow:0 10px 20px rgba(0,0,0,.19),0 6px 6px rgba(0,0,0,.23);font-size:1.5rem}.rotating::before{-webkit-animation:rotating .75s linear infinite;-moz-animation:rotating .75s linear infinite;-ms-animation:rotating .75s linear infinite;-o-animation:rotating .75s linear infinite;animation:rotating .75s linear infinite}.peer_private_key_textbox_switch{position:absolute;right:2rem;transform:translateY(-28px);font-size:1.2rem;cursor:pointer}#peer_private_key_textbox,#private_key,#public_key{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}.progress-bar{transition:.3s ease-in-out}.key{transition:.2s ease-in-out;cursor:pointer}.key:hover{color:#007bff}.card,.form-control{border-radius:10px}.peer_list .card .button-group{height:22px}.btn{border-radius:8px}.modal-content{border-radius:10px}.tooltip-inner{font-size:.8rem}#conf_status_btn,.conf_card{transition:.2s ease-in-out}.conf_card:hover{border-color:#007bff;cursor:pointer}.info_loading{animation:loading 2s infinite ease-in-out;border-radius:5px;height:19px;transition:.3s ease-in-out}#conf_status_btn.info_loading{height:38px;border-radius:5px;animation:loading 3s infinite ease-in-out}#qrcode_img img{width:100%}#selected_ip_list .badge,#selected_peer_list .badge{margin:.1rem}#add_modal.ip_modal_open{transition:filter .2s ease-in-out;filter:brightness(.5)}#delete_bulk_modal .list-group a.active{background-color:#dc3545;border-color:#dc3545}#selected_peer_list{max-height:80px;overflow-y:scroll;overflow-x:hidden}.no-responding,.no-response{transition:all 1s ease-in-out}.no-response{width:100%;height:100%;position:fixed;background:#000000ba;z-index:10000;display:none;flex-direction:column;align-items:center;justify-content:center;opacity:0}.no-response.active{display:flex}.no-response.active.show{opacity:100}.no-response .container>*{text-align:center}.no-responding{filter:blur(10px)}pre.index-alert{margin-bottom:0;padding:1rem;background-color:#343a40;border:1px solid rgba(0,0,0,.125);border-radius:.25rem;margin-top:1rem;color:#fff}
\ No newline at end of file
diff --git a/src/templates/index.html b/src/templates/index.html
index 5e1630e..01f4756 100644
--- a/src/templates/index.html
+++ b/src/templates/index.html
@@ -12,8 +12,13 @@
Home
-{# #}
+ {% if msg != "" %}
+
+ Configuration toggle failed. Please check the following error message:
+
+
{{ msg }}
+ {% endif %}
{% if conf == [] %}
You don't have any WireGuard configurations yet. Please check the configuration folder or change it in "Settings". By default the folder is "/etc/wireguard".