mirror of
https://github.com/donaldzou/WGDashboard.git
synced 2024-11-22 07:10:09 +01:00
v2.2-beta3
Finished QR code function and starting to test.
This commit is contained in:
parent
42bbfbe538
commit
4a04d42600
23
README.md
23
README.md
@ -17,13 +17,14 @@
|
||||
<p align="center">Monitoring WireGuard is not convinient, need to login into server and type <code>wg show</code>. That's why this platform is being created, to view all configurations and manage them in a easier way.</p>
|
||||
|
||||
|
||||
## 📣 What's New: Version 2.1
|
||||
## 📣 What's New: Version v2.2
|
||||
|
||||
- Added **Ping** and **Traceroute** tools!
|
||||
- Adjusted the calculation of data usage on each peers
|
||||
- Added refresh interval of the dashboard
|
||||
- Bug fixed when no configuration on fresh install ([Bug report](https://github.com/donaldzou/wireguard-dashboard/issues/23#issuecomment-869189672))
|
||||
- Fixed crash when too many peers ([Bug report](https://github.com/donaldzou/wireguard-dashboard/issues/22#issuecomment-868840564))
|
||||
- 🎉 **New Features**
|
||||
- **QR Code**: You can add the private key of your existed peer to create a QR code, or just create a new one, dashboard will now be able to auto generate a private key and public key ;) Don't worry, all keys will be generated on your machine, and **will delete all key files after they got generated**.
|
||||
- **Autostart on boot**: Added a tutorial on how to start the dashboard to on boot! Please read the [tutorial below](https://github.com/donaldzou/wireguard-dashboard/tree/v2.2-beta#autostart-wireguard-dashboard-on-boot).
|
||||
- 🪚 **Bug Fixed**
|
||||
- When there are comments in the wireguard config file, will cause the dashboard to crash.
|
||||
- Used regex to search for config files.
|
||||
<hr>
|
||||
|
||||
|
||||
@ -34,10 +35,14 @@
|
||||
- [🛠 Install](https://github.com/donaldzou/wireguard-dashboard#-install)
|
||||
- [🪜 Usage](https://github.com/donaldzou/wireguard-dashboard#-usage)
|
||||
- [✂️ Dashboard Configuration](https://github.com/donaldzou/wireguard-dashboard#%EF%B8%8F-dashboard-configuration)
|
||||
- [Start/Stop/Restart Wireguard Dashboard](https://github.com/donaldzou/wireguard-dashboard/tree/v2.2-beta#startstoprestart-wireguard-dashboard)
|
||||
- [Autostart Wireguard Dashboard on boot](https://github.com/donaldzou/wireguard-dashboard/tree/v2.2-beta#autostart-wireguard-dashboard-on-boot)
|
||||
- [❓ How to update the dashboard?](https://github.com/donaldzou/wireguard-dashboard#-how-to-update-the-dashboard)
|
||||
- [⚠️ Update from v1.x.x](https://github.com/donaldzou/wireguard-dashboard#%EF%B8%8F--update-from-v1xx)
|
||||
- [🔍 Screenshot](https://github.com/donaldzou/wireguard-dashboard#-screenshot)
|
||||
|
||||
|
||||
|
||||
## 💡 Features
|
||||
|
||||
- Add peers for each WireGuard configuration
|
||||
@ -71,7 +76,7 @@
|
||||
1. **Download Wireguard Dashboard**
|
||||
|
||||
```shell
|
||||
$ git clone -b v2.1 https://github.com/donaldzou/Wireguard-Dashboard.git
|
||||
$ git clone -b vv2.2 https://github.com/donaldzou/Wireguard-Dashboard.git
|
||||
2. **Install Python Dependencies**
|
||||
|
||||
```shell
|
||||
@ -262,7 +267,7 @@ All these settings will be able to configure within the dashboard in **Settings*
|
||||
```
|
||||
2. Get the newest version
|
||||
```
|
||||
$ sudo git pull https://github.com/donaldzou/wireguard-dashboard.git v2.1 --force
|
||||
$ sudo git pull https://github.com/donaldzou/wireguard-dashboard.git vv2.2 --force
|
||||
```
|
||||
3. Update and install all python dependencies
|
||||
```
|
||||
@ -275,7 +280,7 @@ All these settings will be able to configure within the dashboard in **Settings*
|
||||
### ⚠️ **Update from v1.x.x**
|
||||
|
||||
1. Stop the dashboard if it is running.
|
||||
2. You can use `git pull https://github.com/donaldzou/Wireguard-Dashboard.git v2.1` to get the new update inside `Wireguard-Dashboard` directory.
|
||||
2. You can use `git pull https://github.com/donaldzou/Wireguard-Dashboard.git vv2.2` to get the new update inside `Wireguard-Dashboard` directory.
|
||||
3. Proceed **Step 2 & 3** in the [Install](#-install) step down below.
|
||||
|
||||
## 🔍 Screenshot
|
||||
|
@ -13,6 +13,7 @@ import configparser
|
||||
import re
|
||||
# PIP installed library
|
||||
import ifcfg
|
||||
from flask_qrcode import QRcode
|
||||
from tinydb import TinyDB, Query
|
||||
from icmplib import ping, multiping, traceroute, resolve, Host, Hop
|
||||
|
||||
@ -20,12 +21,16 @@ from icmplib import ping, multiping, traceroute, resolve, Host, Hop
|
||||
dashboard_version = 'v2.1'
|
||||
# Dashboard Config Name
|
||||
dashboard_conf = 'wg-dashboard.ini'
|
||||
# Default Wireguard IP
|
||||
wg_ip = ifcfg.default_interface()['inet']
|
||||
|
||||
# Upgrade Required
|
||||
update = ""
|
||||
# Flask App Configuration
|
||||
app = Flask("Wireguard Dashboard")
|
||||
app.secret_key = secrets.token_urlsafe(16)
|
||||
app.config['TEMPLATES_AUTO_RELOAD'] = True
|
||||
QRcode(app)
|
||||
|
||||
|
||||
def get_conf_peer_key(config_name):
|
||||
@ -54,7 +59,6 @@ def get_conf_running_peer_number(config_name):
|
||||
count += 2
|
||||
return running
|
||||
|
||||
|
||||
def is_match(regex, text):
|
||||
pattern = re.compile(regex)
|
||||
return pattern.search(text) is not None
|
||||
@ -97,7 +101,6 @@ def read_conf_file(config_name):
|
||||
# Read Configuration File End
|
||||
return conf_peer_data
|
||||
|
||||
|
||||
def get_latest_handshake(config_name, db, peers):
|
||||
# Get latest handshakes
|
||||
try:
|
||||
@ -172,17 +175,17 @@ def get_allowed_ip(config_name, db, peers, conf_peer_data):
|
||||
for i in conf_peer_data["Peers"]:
|
||||
db.update({"allowed_ip": i.get('AllowedIPs', '(None)')}, peers.id == i["PublicKey"])
|
||||
|
||||
|
||||
|
||||
def get_conf_peers_data(config_name):
|
||||
db = TinyDB('db/' + config_name + '.json')
|
||||
peers = Query()
|
||||
conf_peer_data = read_conf_file(config_name)
|
||||
|
||||
for i in conf_peer_data['Peers']:
|
||||
if not db.search(peers.id == i['PublicKey']):
|
||||
search = db.search(peers.id == i['PublicKey'])
|
||||
if not search:
|
||||
db.insert({
|
||||
"id": i['PublicKey'],
|
||||
"private_key": "",
|
||||
"name": "",
|
||||
"total_receive": 0,
|
||||
"total_sent": 0,
|
||||
@ -193,6 +196,10 @@ def get_conf_peers_data(config_name):
|
||||
"allowed_ip": 0,
|
||||
"traffic": []
|
||||
})
|
||||
else:
|
||||
# Update database since V2.2
|
||||
if "private_key" not in search[0]:
|
||||
db.update({'private_key':''}, peers.id == i['PublicKey'])
|
||||
|
||||
tic = time.perf_counter()
|
||||
get_latest_handshake(config_name, db, peers)
|
||||
@ -203,11 +210,6 @@ def get_conf_peers_data(config_name):
|
||||
print(f"Finish fetching data in {toc - tic:0.4f} seconds")
|
||||
db.close()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def get_peers(config_name):
|
||||
get_conf_peers_data(config_name)
|
||||
db = TinyDB('db/' + config_name + '.json')
|
||||
@ -216,7 +218,6 @@ def get_peers(config_name):
|
||||
db.close()
|
||||
return result
|
||||
|
||||
|
||||
def get_conf_pub_key(config_name):
|
||||
conf = configparser.ConfigParser(strict=False)
|
||||
conf.read(wg_conf_path + "/" + config_name + ".conf")
|
||||
@ -225,7 +226,6 @@ def get_conf_pub_key(config_name):
|
||||
conf.clear()
|
||||
return pub.decode().strip("\n")
|
||||
|
||||
|
||||
def get_conf_listen_port(config_name):
|
||||
conf = configparser.ConfigParser(strict=False)
|
||||
conf.read(wg_conf_path + "/" + config_name + ".conf")
|
||||
@ -233,7 +233,6 @@ def get_conf_listen_port(config_name):
|
||||
conf.clear()
|
||||
return port
|
||||
|
||||
|
||||
def get_conf_total_data(config_name):
|
||||
db = TinyDB('db/' + config_name + '.json')
|
||||
upload_total = 0
|
||||
@ -250,7 +249,6 @@ def get_conf_total_data(config_name):
|
||||
db.close()
|
||||
return [total, upload_total, download_total]
|
||||
|
||||
|
||||
def get_conf_status(config_name):
|
||||
ifconfig = dict(ifcfg.interfaces().items())
|
||||
if config_name in ifconfig.keys():
|
||||
@ -258,24 +256,21 @@ def get_conf_status(config_name):
|
||||
else:
|
||||
return "stopped"
|
||||
|
||||
|
||||
def get_conf_list():
|
||||
conf = []
|
||||
for i in os.listdir(wg_conf_path):
|
||||
if not i.startswith('.'):
|
||||
if ".conf" in i:
|
||||
i = i.replace('.conf', '')
|
||||
temp = {"conf": i, "status": get_conf_status(i), "public_key": get_conf_pub_key(i)}
|
||||
if temp['status'] == "running":
|
||||
temp['checked'] = 'checked'
|
||||
else:
|
||||
temp['checked'] = ""
|
||||
conf.append(temp)
|
||||
if is_match("^(.{1,}).(conf)$", i):
|
||||
i = i.replace('.conf', '')
|
||||
temp = {"conf": i, "status": get_conf_status(i), "public_key": get_conf_pub_key(i)}
|
||||
if temp['status'] == "running":
|
||||
temp['checked'] = 'checked'
|
||||
else:
|
||||
temp['checked'] = ""
|
||||
conf.append(temp)
|
||||
if len(conf) > 0:
|
||||
conf = sorted(conf, key=itemgetter('conf'))
|
||||
return conf
|
||||
|
||||
|
||||
@app.before_request
|
||||
def auth_req():
|
||||
conf = configparser.ConfigParser(strict=False)
|
||||
@ -520,7 +515,7 @@ def get_conf(config_name):
|
||||
conf_data['checked'] = "nope"
|
||||
else:
|
||||
conf_data['checked'] = "checked"
|
||||
return render_template('get_conf.html', conf=get_conf_list(), conf_data=conf_data)
|
||||
return render_template('get_conf.html', conf=get_conf_list(), conf_data=conf_data, wg_ip=wg_ip)
|
||||
|
||||
|
||||
@app.route('/switch/<config_name>', methods=['GET'])
|
||||
@ -552,7 +547,7 @@ def add_peer(config_name):
|
||||
if type(keys) != list:
|
||||
return config_name+" is not running."
|
||||
if public_key in keys:
|
||||
return "Key already exist."
|
||||
return "Public key already exist."
|
||||
else:
|
||||
status = ""
|
||||
try:
|
||||
@ -562,7 +557,7 @@ def add_peer(config_name):
|
||||
get_conf_peers_data(config_name)
|
||||
db = TinyDB("db/" + config_name + ".json")
|
||||
peers = Query()
|
||||
db.update({"name": data['name']}, peers.id == public_key)
|
||||
db.update({"name": data['name'], "private_key": data['private_key']}, peers.id == public_key)
|
||||
db.close()
|
||||
return "true"
|
||||
except subprocess.CalledProcessError as exc:
|
||||
@ -617,8 +612,37 @@ def get_peer_name(config_name):
|
||||
db.close()
|
||||
return result[0]['name']
|
||||
|
||||
@app.route('/generate_peer', methods=['GET'])
|
||||
def generate_peer():
|
||||
gen = subprocess.check_output('wg genkey > private_key.txt && wg pubkey < private_key.txt > public_key.txt',shell=True)
|
||||
private = open('private_key.txt')
|
||||
private_key = private.readline().strip()
|
||||
public = open('public_key.txt')
|
||||
public_key = public.readline().strip()
|
||||
data = {"private_key": private_key, "public_key": public_key}
|
||||
private.close()
|
||||
public.close()
|
||||
os.remove('private_key.txt')
|
||||
os.remove('public_key.txt')
|
||||
return jsonify(data)
|
||||
|
||||
|
||||
@app.route('/generate_public_key', methods=['POST'])
|
||||
def generate_public_key():
|
||||
data = request.get_json()
|
||||
private_key = data['private_key']
|
||||
pri_key_file = open('private_key.txt', 'w')
|
||||
pri_key_file.write(private_key)
|
||||
pri_key_file.close()
|
||||
try:
|
||||
check = subprocess.check_output("wg pubkey < private_key.txt > public_key.txt", shell=True)
|
||||
public = open('public_key.txt')
|
||||
public_key = public.readline().strip()
|
||||
os.remove('private_key.txt')
|
||||
os.remove('public_key.txt')
|
||||
return jsonify({"status":'success', "msg":"", "data":public_key})
|
||||
except subprocess.CalledProcessError as exc:
|
||||
os.remove('private_key.txt')
|
||||
return jsonify({"status":'failed', "msg":"Key is not the correct length or format", "data":""})
|
||||
|
||||
def init_dashboard():
|
||||
# Set Default INI File
|
||||
|
1
src/private_key.txt
Normal file
1
src/private_key.txt
Normal file
@ -0,0 +1 @@
|
||||
NZV8Z5QOobhbM4f6RLtICNghumYT30aKUxaUNmEtulM=
|
1
src/public_key.txt
Normal file
1
src/public_key.txt
Normal file
@ -0,0 +1 @@
|
||||
NZV8Z5QOobhbM4f6RLtICNghumYT30aKUxaUNmEtulM=
|
@ -195,4 +195,40 @@ main{
|
||||
/* padding: 0.75rem 1.5rem;*/
|
||||
/* border-radius: 3rem;*/
|
||||
/* font-size: 1rem;*/
|
||||
/*}*/
|
||||
/*}*/
|
||||
|
||||
@-webkit-keyframes rotating /* Safari and Chrome */ {
|
||||
from {
|
||||
-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 {
|
||||
from {
|
||||
-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);
|
||||
}
|
||||
}
|
||||
.rotating::before {
|
||||
-webkit-animation: rotating 0.75s linear infinite;
|
||||
-moz-animation: rotating 0.75s linear infinite;
|
||||
-ms-animation: rotating 0.75s linear infinite;
|
||||
-o-animation: rotating 0.75s linear infinite;
|
||||
animation: rotating 0.75s linear infinite;
|
||||
}
|
@ -23,16 +23,32 @@
|
||||
</button>
|
||||
</div>
|
||||
<form id="add_peer_form">
|
||||
<div class="form-group">
|
||||
<label for="public_key">Public Key<code>*</code></label>
|
||||
<input type="text" class="form-control" id="public_key" aria-describedby="public_key">
|
||||
<div class="alert alert-warning" role="alert" style="font-size: 0.8rem">
|
||||
To generate QR code for this new peer, you need to provide the private key, or use the generated key. If you don't need the QR code, simply remove the private key and insert your existed public key.
|
||||
|
||||
</div>
|
||||
<div>
|
||||
<label for="private_key">Private Key</label>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="private_key" aria-describedby="public_key">
|
||||
<div class="input-group-append">
|
||||
<button type="button" class="btn btn-danger" id="re_generate_key">
|
||||
<i class="bi bi-arrow-repeat"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="allowed_ips">Allowed IPs<code>*</code></label>
|
||||
<label for="public_key">Public Key <code>(Required)</code></label>
|
||||
<input type="text" class="form-control" id="public_key" aria-describedby="public_key" disabled>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="allowed_ips">Allowed IPs <code>(Required)</code></label>
|
||||
<input type="text" class="form-control" id="allowed_ips">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="allowed_ips">Name</label>
|
||||
<label for="new_add_name">Name</label>
|
||||
<input type="text" class="form-control" id="new_add_name">
|
||||
</div>
|
||||
</form>
|
||||
@ -95,6 +111,23 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="qrcode_modal" data-backdrop="static" data-keyboard="false" tabindex="-1"
|
||||
aria-labelledby="staticBackdropLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="peer_name">QR Code</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<img src="" id="qrcode_img" style="width: 100%">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="position-fixed top-0 right-0 p-3" style="z-index: 5; right: 0; top: 50px;">
|
||||
<div id="alertToast" class="toast hide" role="alert" aria-live="assertive" aria-atomic="true" data-delay="5000">
|
||||
<div class="toast-header">
|
||||
@ -148,14 +181,63 @@
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
function generate_key(){
|
||||
$.ajax({
|
||||
"url": "/generate_peer",
|
||||
"method": "GET",
|
||||
}).done(function(res){
|
||||
$("#private_key").val(res.private_key)
|
||||
$("#public_key").val(res.public_key)
|
||||
$("#add_peer_alert").addClass("d-none");
|
||||
$("#re_generate_key i").removeClass("rotating")
|
||||
})
|
||||
}
|
||||
function generate_public_key(){
|
||||
$.ajax({
|
||||
"url": "/generate_public_key",
|
||||
"method": "POST",
|
||||
"headers":{"Content-Type": "application/json"},
|
||||
"data": JSON.stringify({"private_key": $("#private_key").val()})
|
||||
}).done(function(res){
|
||||
if(res['status'] === "failed"){
|
||||
$("#add_peer_alert").html(res['msg']+$("#add_peer_alert").html());
|
||||
$("#add_peer_alert").removeClass("d-none");
|
||||
}else{
|
||||
$("#add_peer_alert").addClass("d-none");
|
||||
}
|
||||
$("#public_key").val(res['data'])
|
||||
$("#re_generate_key i").removeClass("rotating")
|
||||
})
|
||||
}
|
||||
|
||||
$("#private_key").change(function(){
|
||||
if ($("#private_key").val().length > 0){
|
||||
$("#re_generate_key i").addClass("rotating")
|
||||
generate_public_key()
|
||||
}else{
|
||||
$("#public_key").removeAttr("disabled")
|
||||
$("#public_key").val("")
|
||||
}
|
||||
})
|
||||
$('#add_modal').on('show.bs.modal', function (event) {
|
||||
generate_key()
|
||||
})
|
||||
$("#re_generate_key").click(function (){
|
||||
$("#public_key").attr("disabled","disabled")
|
||||
$("#re_generate_key i").addClass("rotating")
|
||||
generate_key()
|
||||
})
|
||||
|
||||
$("body").on("click", ".switch", function (){
|
||||
$(this).siblings($(".spinner-border")).css("display", "inline-block");
|
||||
$(this).remove()
|
||||
location.replace("/switch/"+$(this).attr('id'));
|
||||
})
|
||||
|
||||
|
||||
|
||||
$("#save_peer").click(function(){
|
||||
if ($("#allowed_ips") != "" && $("#public_key") != ""){
|
||||
if ($("#allowed_ips") !== "" && $("#public_key") !== ""){
|
||||
var conf = $(this).attr('conf_id')
|
||||
$.ajax({
|
||||
method: "POST",
|
||||
@ -163,9 +245,12 @@
|
||||
headers:{
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
data: JSON.stringify({"public_key":$("#public_key").val(),
|
||||
data: JSON.stringify({
|
||||
"private_key":$("#private_key").val(),
|
||||
"public_key":$("#public_key").val(),
|
||||
"allowed_ips": $("#allowed_ips").val(),
|
||||
"name":$("#new_add_name").val()}),
|
||||
"name":$("#new_add_name").val()
|
||||
}),
|
||||
success: function (response){
|
||||
if(response != "true"){
|
||||
$("#add_peer_alert").html(response+$("#add_peer_alert").html());
|
||||
@ -178,8 +263,14 @@
|
||||
})
|
||||
}
|
||||
})
|
||||
var qrcodeModal = new bootstrap.Modal(document.getElementById('qrcode_modal'), {
|
||||
keyboard: false
|
||||
})
|
||||
|
||||
|
||||
$("body").on("click", ".btn-qrcode-peer", function (){
|
||||
qrcodeModal.toggle();
|
||||
$("#qrcode_img").attr('src', $(this).attr('img_src'))
|
||||
})
|
||||
|
||||
var deleteModal = new bootstrap.Modal(document.getElementById('delete_modal'), {
|
||||
keyboard: false
|
||||
|
@ -111,6 +111,11 @@
|
||||
<hr>
|
||||
<button type="button" class="btn btn-outline-primary btn-setting-peer btn-control" id="{{i['id']}}" data-toggle="modal"><i class="bi bi-gear-fill"></i></button>
|
||||
<button type="button" class="btn btn-outline-danger btn-delete-peer btn-control" id="{{i['id']}}" data-toggle="modal"><i class="bi bi-x-circle-fill"></i></button>
|
||||
{% if i['private_key'] %}
|
||||
<button type="button" class="btn btn-outline-success btn-qrcode-peer btn-control" img_src="{{ qrcode("[Interface]\nPrivateKey = "+i['private_key']+"\nAddress = "+i['allowed_ip']+"\nDNS = 1.1.1.1\n\n[Peer]\nPublicKey = "+conf_data['public_key']+"\nAllowedIPs = 0.0.0.0/0\nEndpoint = "+wg_ip+":"+conf_data['listen_port']) }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" style="width: 19px;" fill="#28a745"><path d="M3 11h8V3H3v8zm2-6h4v4H5V5zM3 21h8v-8H3v8zm2-6h4v4H5v-4zM13 3v8h8V3h-8zm6 6h-4V5h4v4zM13 13h2v2h-2zM15 15h2v2h-2zM13 17h2v2h-2zM17 17h2v2h-2zM19 19h2v2h-2zM15 19h2v2h-2zM17 13h2v2h-2zM19 15h2v2h-2z"/></svg>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -3,7 +3,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<title>Wireguard Dashboard</title>
|
||||
<link rel="icon" href="{{ url_for('static',filename='logo.png') }}"/>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css" integrity="sha384-B0vP5xmATw1+K9KRQjQERJvTumQW0nPEzvF6L/Z6nronJ3oUOFUFpCjEUQouq2+l" crossorigin="anonymous">
|
||||
<link rel= "stylesheet" type= "text/css" href= "{{ url_for('static',filename='dashboard.css') }}">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.4.1/font/bootstrap-icons.css">
|
||||
</head>
|
||||
|
Loading…
Reference in New Issue
Block a user