1
0
mirror of https://github.com/donaldzou/WGDashboard.git synced 2024-11-06 16:00:28 +01:00

I am giving up on using ORM...

Lets go back to the good old sql query days ;)
This commit is contained in:
Donald Zou 2024-02-11 23:53:51 -05:00
parent 6b6ad05e3a
commit 1e88491ca1
11 changed files with 665 additions and 171 deletions

View File

@ -1,3 +1,4 @@
import itertools
import sqlite3 import sqlite3
import configparser import configparser
import hashlib import hashlib
@ -19,9 +20,9 @@ from operator import itemgetter
from typing import Dict, Any from typing import Dict, Any
import bcrypt import bcrypt
import flask
# PIP installed library # PIP installed library
import ifcfg import ifcfg
import psutil
import pyotp import pyotp
from flask import Flask, request, render_template, redirect, url_for, session, jsonify, g from flask import Flask, request, render_template, redirect, url_for, session, jsonify, g
from flask.json.provider import JSONProvider from flask.json.provider import JSONProvider
@ -57,7 +58,7 @@ QRcode(app)
''' '''
Classes Classes
''' '''
Base = declarative_base() # Base = declarative_base(class_registry=dict())
class CustomJsonEncoder(JSONProvider): class CustomJsonEncoder(JSONProvider):
@ -73,25 +74,40 @@ class CustomJsonEncoder(JSONProvider):
app.json = CustomJsonEncoder(app) app.json = CustomJsonEncoder(app)
class Peer:
def __init__(self, tableData):
# for i in range(0, len(table)):
# tableData[table.description[i][0]] = table[i]
self.id = tableData["id"]
self.private_key = tableData["private_key"]
self.DNS = tableData["DNS"]
self.endpoint_allowed_ip = tableData["endpoint_allowed_ip"]
self.name = tableData["name"]
self.total_receive = tableData["total_receive"]
self.total_sent = tableData["total_sent"]
self.total_data = tableData["total_data"]
self.endpoint = tableData["endpoint"]
self.status = tableData["status"]
self.latest_handshake = tableData["latest_handshake"]
self.allowed_ip = tableData["allowed_ip"]
self.cumu_receive = tableData["cumu_receive"]
self.cumu_sent = tableData["cumu_sent"]
self.cumu_data = tableData["cumu_data"]
self.mtu = tableData["mtu"]
self.keepalive = tableData["keepalive"]
self.remote_endpoint = tableData["remote_endpoint"]
self.preshared_key = tableData["preshared_key"]
def toJson(self):
return json.dumps(self, default=lambda o: o.__dict__,
sort_keys=True, indent=4)
def __repr__(self):
return self.toJson()
class WireguardConfiguration: class WireguardConfiguration:
__parser: configparser.ConfigParser = configparser.ConfigParser(strict=False)
__parser.optionxform = str
Status: bool = False
Name: str = ""
PrivateKey: str = ""
PublicKey: str = ""
ListenPort: str = ""
Address: str = ""
DNS: str = ""
Table: str = ""
MTU: str = ""
PreUp: str = ""
PostUp: str = ""
PreDown: str = ""
PostDown: str = ""
SaveConfig: bool = False
class InvalidConfigurationFileException(Exception): class InvalidConfigurationFileException(Exception):
def __init__(self, m): def __init__(self, m):
self.message = m self.message = m
@ -99,10 +115,28 @@ class WireguardConfiguration:
def __str__(self): def __str__(self):
return self.message return self.message
def __init__(self, name: str = None): def __init__(self, name: str = None, data: dict = None):
self.__parser: configparser.ConfigParser = configparser.ConfigParser(strict=False)
self.__parser.optionxform = str
self.Status: bool = False
self.Name: str = ""
self.PrivateKey: str = ""
self.PublicKey: str = ""
self.ListenPort: str = ""
self.Address: str = ""
self.DNS: str = ""
self.Table: str = ""
self.MTU: str = ""
self.PreUp: str = ""
self.PostUp: str = ""
self.PreDown: str = ""
self.PostDown: str = ""
self.SaveConfig: bool = True
if name is not None: if name is not None:
self.Name = name self.Name = name
self.__parser.read(os.path.join(WG_CONF_PATH, f'{self.Name}.conf')) self.__parser.read_file(open(os.path.join(WG_CONF_PATH, f'{self.Name}.conf')))
sections = self.__parser.sections() sections = self.__parser.sections()
if "Interface" not in sections: if "Interface" not in sections:
raise self.InvalidConfigurationFileException( raise self.InvalidConfigurationFileException(
@ -118,33 +152,195 @@ class WireguardConfiguration:
if self.PrivateKey: if self.PrivateKey:
self.PublicKey = self.__getPublicKey() self.PublicKey = self.__getPublicKey()
self.Status = self.__getStatus() self.Status = self.getStatus()
# Create tables in database
self.__createDatabase() else:
self.Name = data["ConfigurationName"]
for i in dir(self):
if str(i) in data.keys():
if isinstance(getattr(self, i), bool):
setattr(self, i, _strToBool(data[i]))
else:
setattr(self, i, str(data[i]))
# self.__createDatabase()
self.__parser["Interface"] = {
"PrivateKey": self.PrivateKey,
"Address": self.Address,
"ListenPort": self.ListenPort,
"PreUp": self.PreUp,
"PreDown": self.PreDown,
"PostUp": self.PostUp,
"PostDown": self.PostDown,
"SaveConfig": "true"
}
with open(os.path.join(DashboardConfig.GetConfig("Server", "wg_conf_path")[1],
f"{self.Name}.conf"), "w+") as configFile:
print(self.__parser.sections())
self.__parser.write(configFile)
self.Peers = []
# Create tables in database
self.__createDatabase()
self.__getPeers()
def __createDatabase(self): def __createDatabase(self):
inspector = inspect(engine) existingTables = cursor.execute("SELECT name FROM sqlite_master WHERE type='table'").fetchall()
existingTable = inspector.get_table_names() existingTables = itertools.chain(*existingTables)
if self.Name not in existingTable: if self.Name not in existingTables:
_createPeerModel(self.Name).__table__.create(engine) cursor.execute(
if self.Name + "_restrict_access" not in existingTable: """
_createRestrcitedPeerModel(self.Name).__table__.create(engine) CREATE TABLE %s (
if self.Name + "_transfer" not in existingTable: id VARCHAR NOT NULL, private_key VARCHAR NULL, DNS VARCHAR NULL,
_createPeerTransferModel(self.Name).__table__.create(engine) endpoint_allowed_ip VARCHAR NULL, name VARCHAR NULL, total_receive FLOAT NULL,
total_sent FLOAT NULL, total_data FLOAT NULL, endpoint VARCHAR NULL,
status VARCHAR NULL, latest_handshake VARCHAR NULL, allowed_ip VARCHAR NULL,
cumu_receive FLOAT NULL, cumu_sent FLOAT NULL, cumu_data FLOAT NULL, mtu INT NULL,
keepalive INT NULL, remote_endpoint VARCHAR NULL, preshared_key VARCHAR NULL,
PRIMARY KEY (id)
)
""" % self.Name
)
sqldb.commit()
if f'{self.Name}_restrict_access' not in existingTables:
cursor.execute(
"""
CREATE TABLE %s_restrict_access (
id VARCHAR NOT NULL, private_key VARCHAR NULL, DNS VARCHAR NULL,
endpoint_allowed_ip VARCHAR NULL, name VARCHAR NULL, total_receive FLOAT NULL,
total_sent FLOAT NULL, total_data FLOAT NULL, endpoint VARCHAR NULL,
status VARCHAR NULL, latest_handshake VARCHAR NULL, allowed_ip VARCHAR NULL,
cumu_receive FLOAT NULL, cumu_sent FLOAT NULL, cumu_data FLOAT NULL, mtu INT NULL,
keepalive INT NULL, remote_endpoint VARCHAR NULL, preshared_key VARCHAR NULL,
PRIMARY KEY (id)
)
""" % self.Name
)
sqldb.commit()
if f'{self.Name}_transfer' not in existingTables:
cursor.execute(
"""
CREATE TABLE %s_transfer (
id VARCHAR NOT NULL, total_receive FLOAT NULL,
total_sent FLOAT NULL, total_data FLOAT NULL,
cumu_receive FLOAT NULL, cumu_sent FLOAT NULL, cumu_data FLOAT NULL, time DATETIME
)
""" % self.Name
)
sqldb.commit()
def __getPublicKey(self) -> str: def __getPublicKey(self) -> str:
return subprocess.check_output(['wg', 'pubkey'], input=self.PrivateKey.encode()).decode().strip('\n') return subprocess.check_output(['wg', 'pubkey'], input=self.PrivateKey.encode()).decode().strip('\n')
def __getStatus(self) -> bool: def getStatus(self) -> bool:
return self.Name in dict(ifcfg.interfaces().items()).keys() self.Status = self.Name in psutil.net_if_addrs().keys()
return self.Status
def __getPeers(self):
with open(os.path.join(WG_CONF_PATH, f'{self.Name}.conf'), 'r') as configFile:
p = []
pCounter = -1
content = configFile.read().split('\n')
try:
peerStarts = content.index("[Peer]")
content = content[peerStarts:]
for i in content:
if not regex_match("#(.*)", i) and not regex_match(";(.*)", i):
if i == "[Peer]":
pCounter += 1
p.append({})
else:
if len(i) > 0:
split = re.split(r'\s*=\s*', i, 1)
if len(split) == 2:
p[pCounter][split[0]] = split[1]
for i in p:
if "PublicKey" in i.keys():
checkIfExist = cursor.execute("SELECT * FROM %s WHERE id = ?" % self.Name,
((i['PublicKey']),)).fetchone()
if checkIfExist is None:
newPeer = {
"id": i['PublicKey'],
"private_key": "",
"DNS": DashboardConfig.GetConfig("Peers", "peer_global_DNS")[1],
"endpoint_allowed_ip": DashboardConfig.GetConfig("Peers", "peer_endpoint_allowed_ip")[
1],
"name": "",
"total_receive": 0,
"total_sent": 0,
"total_data": 0,
"endpoint": "N/A",
"status": "stopped",
"latest_handshake": "N/A",
"allowed_ip": "N/A",
"cumu_receive": 0,
"cumu_sent": 0,
"cumu_data": 0,
"traffic": [],
"mtu": DashboardConfig.GetConfig("Peers", "peer_mtu")[1],
"keepalive": DashboardConfig.GetConfig("Peers", "peer_keep_alive")[1],
"remote_endpoint": DashboardConfig.GetConfig("Peers", "remote_endpoint")[1],
"preshared_key": i["PresharedKey"] if "PresharedKey" in i.keys() else ""
}
cursor.execute(
"""
INSERT INTO %s
VALUES (:id, :private_key, :DNS, :endpoint_allowed_ip, :name, :total_receive, :total_sent,
:total_data, :endpoint, :status, :latest_handshake, :allowed_ip, :cumu_receive, :cumu_sent,
:cumu_data, :mtu, :keepalive, :remote_endpoint, :preshared_key);
""" % self.Name
, newPeer)
sqldb.commit()
else:
self.Peers.append(Peer(checkIfExist))
except ValueError:
pass
print(self.Peers)
def toggleConfiguration(self) -> [bool, str]:
self.getStatus()
print("Status: ", self.getStatus())
if self.Status:
try:
check = subprocess.check_output(f"wg-quick down {self.Name}",
shell=True, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as exc:
return False, str(exc.output.strip().decode("utf-8"))
else:
try:
check = subprocess.check_output(f"wg-quick up {self.Name}",
shell=True, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as exc:
return False, str(exc.output.strip().decode("utf-8"))
self.getStatus()
return True, None
def toJSON(self): def toJSON(self):
self.Status = self.__getStatus() self.Status = self.getStatus()
return self.__dict__ return {
"Status": self.Status,
"Name": self.Name,
"PrivateKey": self.PrivateKey,
"PublicKey": self.PublicKey,
"Address": self.Address,
"ListenPort": self.ListenPort,
"PreUp": self.PreUp,
"PreDown": self.PreDown,
"PostUp": self.PostUp,
"PostDown": self.PostDown,
"SaveConfig": self.SaveConfig
}
def newConfiguration(self):
pass # Regex Match
def regex_match(regex, text):
pattern = re.compile(regex)
return pattern.search(text) is not None
def iPv46RegexCheck(ip): def iPv46RegexCheck(ip):
@ -157,7 +353,7 @@ class DashboardConfig:
def __init__(self): def __init__(self):
self.__config = configparser.ConfigParser(strict=False) self.__config = configparser.ConfigParser(strict=False)
self.__config.read(DASHBOARD_CONF) self.__config.read_file(open(DASHBOARD_CONF))
self.hiddenAttribute = ["totp_key"] self.hiddenAttribute = ["totp_key"]
self.__default = { self.__default = {
"Account": { "Account": {
@ -309,7 +505,7 @@ def ResponseObject(status=True, message=None, data=None) -> Flask.response_class
DashboardConfig = DashboardConfig() DashboardConfig = DashboardConfig()
WireguardConfigurations: [WireguardConfiguration] = [] WireguardConfigurations: {str: WireguardConfiguration} = {}
''' '''
Private Functions Private Functions
@ -320,71 +516,71 @@ def _strToBool(value: str) -> bool:
return value.lower() in ("yes", "true", "t", "1", 1) return value.lower() in ("yes", "true", "t", "1", 1)
def _createPeerModel(wgConfigName): # def _createPeerModel(wgConfigName):
class Peer(Base): # return type(wgConfigName, (Base,), {
__tablename__ = wgConfigName # "id": mapped_column(VARCHAR, primary_key=True),
id = mapped_column(VARCHAR, primary_key=True) # "private_key": mapped_column(VARCHAR),
private_key = mapped_column(VARCHAR) # "DNS": mapped_column(VARCHAR),
DNS = mapped_column(VARCHAR) # "endpoint_allowed_ip": mapped_column(VARCHAR),
endpoint_allowed_ip = mapped_column(VARCHAR) # "name": mapped_column(VARCHAR),
name = mapped_column(VARCHAR) # "total_receive": mapped_column(FLOAT),
total_receive = mapped_column(FLOAT) # "total_sent": mapped_column(FLOAT),
total_sent = mapped_column(FLOAT) # "total_data": mapped_column(FLOAT),
total_data = mapped_column(FLOAT) # "endpoint": mapped_column(VARCHAR),
endpoint = mapped_column(VARCHAR) # "status": mapped_column(VARCHAR),
status = mapped_column(VARCHAR) # "latest_handshake": mapped_column(VARCHAR),
latest_handshake = mapped_column(VARCHAR) # "allowed_ip": mapped_column(VARCHAR),
allowed_ip = mapped_column(VARCHAR) # "cumu_receive": mapped_column(FLOAT),
cumu_receive = mapped_column(FLOAT) # "cumu_sent": mapped_column(FLOAT),
cumu_sent = mapped_column(FLOAT) # "cumu_data": mapped_column(FLOAT),
cumu_data = mapped_column(FLOAT) # "mtu": mapped_column(INT),
mtu = mapped_column(INT) # "keepalive": mapped_column(INT),
keepalive = mapped_column(INT) # "remote_endpoint": mapped_column(VARCHAR),
remote_endpoint = mapped_column(VARCHAR) # "preshared_key": mapped_column(VARCHAR),
preshared_key = mapped_column(VARCHAR) # "__tablename__": wgConfigName,
# "__table_args__": {'extend_existing': True}
return Peer # })
#
#
def _createRestrcitedPeerModel(wgConfigName): # def _createRestrictedPeerModel(wgConfigName):
class PeerRestricted(Base): # return type(wgConfigName + "_restrict_access", (Base,), {
__tablename__ = wgConfigName + "_restrict_access" # "id": mapped_column(VARCHAR, primary_key=True),
id = mapped_column(VARCHAR, primary_key=True) # "private_key": mapped_column(VARCHAR),
private_key = mapped_column(VARCHAR) # "DNS": mapped_column(VARCHAR),
DNS = mapped_column(VARCHAR) # "endpoint_allowed_ip": mapped_column(VARCHAR),
endpoint_allowed_ip = mapped_column(VARCHAR) # "name": mapped_column(VARCHAR),
name = mapped_column(VARCHAR) # "total_receive": mapped_column(FLOAT),
total_receive = mapped_column(FLOAT) # "total_sent": mapped_column(FLOAT),
total_sent = mapped_column(FLOAT) # "total_data": mapped_column(FLOAT),
total_data = mapped_column(FLOAT) # "endpoint": mapped_column(VARCHAR),
endpoint = mapped_column(VARCHAR) # "status": mapped_column(VARCHAR),
status = mapped_column(VARCHAR) # "latest_handshake": mapped_column(VARCHAR),
latest_handshake = mapped_column(VARCHAR) # "allowed_ip": mapped_column(VARCHAR),
allowed_ip = mapped_column(VARCHAR) # "cumu_receive": mapped_column(FLOAT),
cumu_receive = mapped_column(FLOAT) # "cumu_sent": mapped_column(FLOAT),
cumu_sent = mapped_column(FLOAT) # "cumu_data": mapped_column(FLOAT),
cumu_data = mapped_column(FLOAT) # "mtu": mapped_column(INT),
mtu = mapped_column(INT) # "keepalive": mapped_column(INT),
keepalive = mapped_column(INT) # "remote_endpoint": mapped_column(VARCHAR),
remote_endpoint = mapped_column(VARCHAR) # "preshared_key": mapped_column(VARCHAR),
preshared_key = mapped_column(VARCHAR) # "__tablename__": wgConfigName,
# "__table_args__": {'extend_existing': True}
return PeerRestricted # })
#
#
def _createPeerTransferModel(wgConfigName): # def _createPeerTransferModel(wgConfigName):
class PeerTransfer(Base): # return type(wgConfigName + "_transfer", (Base,), {
__tablename__ = wgConfigName + "_transfer" # "id": mapped_column(VARCHAR, primary_key=True),
id = mapped_column(VARCHAR, primary_key=True) # "total_receive": mapped_column(FLOAT),
total_receive = mapped_column(FLOAT) # "total_sent": mapped_column(FLOAT),
total_sent = mapped_column(FLOAT) # "total_data": mapped_column(FLOAT),
total_data = mapped_column(FLOAT) # "cumu_receive": mapped_column(FLOAT),
cumu_receive = mapped_column(FLOAT) # "cumu_sent": mapped_column(FLOAT),
cumu_sent = mapped_column(FLOAT) # "cumu_data": mapped_column(FLOAT),
cumu_data = mapped_column(FLOAT) # "time": mapped_column(DATETIME),
time = mapped_column(DATETIME) # "__tablename__": wgConfigName + "_transfer",
# "__table_args__": {'extend_existing': True},
return PeerTransfer # })
def _regexMatch(regex, text): def _regexMatch(regex, text):
@ -393,12 +589,12 @@ def _regexMatch(regex, text):
def _getConfigurationList() -> [WireguardConfiguration]: def _getConfigurationList() -> [WireguardConfiguration]:
configurations = [] configurations = {}
for i in os.listdir(WG_CONF_PATH): for i in os.listdir(WG_CONF_PATH):
if _regexMatch("^(.{1,}).(conf)$", i): if _regexMatch("^(.{1,}).(conf)$", i):
i = i.replace('.conf', '') i = i.replace('.conf', '')
try: try:
configurations.append(WireguardConfiguration(i)) configurations[i] = WireguardConfiguration(i)
except WireguardConfiguration.InvalidConfigurationFileException as e: except WireguardConfiguration.InvalidConfigurationFileException as e:
print(f"{i} have an invalid configuration file.") print(f"{i} have an invalid configuration file.")
return configurations return configurations
@ -471,7 +667,7 @@ def API_SignOut():
@app.route('/api/getWireguardConfigurations', methods=["GET"]) @app.route('/api/getWireguardConfigurations', methods=["GET"])
def API_getWireguardConfigurations(): def API_getWireguardConfigurations():
WireguardConfigurations = _getConfigurationList() WireguardConfigurations = _getConfigurationList()
return ResponseObject(data=[wc.toJSON() for wc in WireguardConfigurations]) return ResponseObject(data=[wc.toJSON() for wc in WireguardConfigurations.values()])
@app.route('/api/addWireguardConfiguration', methods=["POST"]) @app.route('/api/addWireguardConfiguration', methods=["POST"])
@ -488,12 +684,46 @@ def API_addWireguardConfiguration():
"PreDown", "PreDown",
"PostUp", "PostUp",
"PostDown", "PostDown",
"UsePreSharedKey"
] ]
requiredKeys = [ requiredKeys = [
"ConfigurationName", "Address", "ListenPort", "PrivateKey" "ConfigurationName", "Address", "ListenPort", "PrivateKey"
] ]
for i in keys:
if i not in data.keys() or (i in requiredKeys and len(str(data[i])) == 0):
return ResponseObject(False, "Please provide all required parameters.")
# Check duplicate names, ports, address
for i in WireguardConfigurations.values():
if i.Name == data['ConfigurationName']:
return ResponseObject(False,
f"Already have a configuration with the name \"{data['ConfigurationName']}\"",
"ConfigurationName")
if str(i.ListenPort) == str(data["ListenPort"]):
return ResponseObject(False,
f"Already have a configuration with the port \"{data['ListenPort']}\"",
"ListenPort")
if i.Address == data["Address"]:
return ResponseObject(False,
f"Already have a configuration with the address \"{data['Address']}\"",
"Address")
WireguardConfigurations[data['ConfigurationName']] = WireguardConfiguration(data=data)
return ResponseObject()
@app.route('/api/toggleWireguardConfiguration/')
def API_toggleWireguardConfiguration():
configurationName = request.args.get('configurationName')
if configurationName is None or len(
configurationName) == 0 or configurationName not in WireguardConfigurations.keys():
return ResponseObject(False, "Please provide a valid configuration name")
toggleStatus, msg = WireguardConfigurations[configurationName].toggleConfiguration()
return ResponseObject(toggleStatus, msg, WireguardConfigurations[configurationName].Status)
@app.route('/api/getDashboardConfiguration', methods=["GET"]) @app.route('/api/getDashboardConfiguration', methods=["GET"])
@ -590,11 +820,75 @@ def index():
return render_template('index_new.html') return render_template('index_new.html')
def backGroundThread():
print("Waiting 5 sec")
time.sleep(5)
while True:
for c in WireguardConfigurations.values():
if c.getStatus():
try:
data_usage = subprocess.check_output(f"wg show {c.Name} transfer",
shell=True, stderr=subprocess.STDOUT)
data_usage = data_usage.decode("UTF-8").split("\n")
data_usage = [p.split("\t") for p in data_usage]
for i in range(len(data_usage)):
if len(data_usage[i]) == 3:
cur_i = cursor.execute(
"SELECT total_receive, total_sent, cumu_receive, cumu_sent, status FROM %s WHERE id= ? "
% c.Name, (data_usage[i][0], )).fetchone()
if cur_i is not None:
total_sent = cur_i['total_sent']
total_receive = cur_i['total_receive']
cur_total_sent = round(int(data_usage[i][2]) / (1024 ** 3), 4)
cur_total_receive = round(int(data_usage[i][1]) / (1024 ** 3), 4)
cumulative_receive = cur_i['cumu_receive'] + total_receive
cumulative_sent = cur_i['cumu_sent'] + total_sent
if total_sent <= cur_total_sent and total_receive <= cur_total_receive:
total_sent = cur_total_sent
total_receive = cur_total_receive
else:
cursor.execute(
"UPDATE %s SET cumu_receive = ?, cumu_sent = ?, cumu_data = ? WHERE id = ?" %
c.Name, (round(cumulative_receive, 4), round(cumulative_sent, 4),
round(cumulative_sent + cumulative_receive, 4),
data_usage[i][0], ))
total_sent = 0
total_receive = 0
cursor.execute(
"UPDATE %s SET total_receive = ?, total_sent = ?, total_data = ? WHERE id = ?"
% c.Name, (round(total_receive, 4), round(total_sent, 4),
round(total_receive + total_sent, 4), data_usage[i][0], ))
now = datetime.now()
now_string = now.strftime("%d/%m/%Y %H:%M:%S")
cursor.execute(f'''
INSERT INTO %s_transfer
(id, total_receive, total_sent, total_data,
cumu_receive, cumu_sent, cumu_data, time)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
''' % c.Name, (data_usage[i][0], round(total_receive, 4), round(total_sent, 4),
round(total_receive + total_sent, 4), round(cumulative_receive, 4),
round(cumulative_sent, 4),
round(cumulative_sent + cumulative_receive, 4), now_string, ))
sqldb.commit()
print(data_usage)
pass
except Exception as e:
print(str(e))
time.sleep(30)
if __name__ == "__main__": if __name__ == "__main__":
engine = create_engine("sqlite:///" + os.path.join(CONFIGURATION_PATH, 'db', 'wgdashboard.db')) engine = create_engine("sqlite:///" + os.path.join(CONFIGURATION_PATH, 'db', 'wgdashboard.db'))
sqldb = sqlite3.connect(os.path.join(CONFIGURATION_PATH, 'db', 'wgdashboard.db'), check_same_thread=False)
sqldb.row_factory = sqlite3.Row
cursor = sqldb.cursor()
_, app_ip = DashboardConfig.GetConfig("Server", "app_ip") _, app_ip = DashboardConfig.GetConfig("Server", "app_ip")
_, app_port = DashboardConfig.GetConfig("Server", "app_port") _, app_port = DashboardConfig.GetConfig("Server", "app_port")
_, WG_CONF_PATH = DashboardConfig.GetConfig("Server", "wg_conf_path") _, WG_CONF_PATH = DashboardConfig.GetConfig("Server", "wg_conf_path")
WireguardConfigurations = _getConfigurationList() WireguardConfigurations = _getConfigurationList()
app.run(host=app_ip, debug=True, port=app_port) bgThread = threading.Thread(target=backGroundThread)
bgThread.daemon = True
bgThread.start()
app.run(host=app_ip, debug=False, port=app_port)

View File

@ -1,9 +1,11 @@
<script> <script>
import {wgdashboardStore} from "@/stores/wgdashboardStore.js"; import {wgdashboardStore} from "@/stores/wgdashboardStore.js";
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js"; import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
import ConfigurationCard from "@/components/configurationListComponents/configurationCard.vue";
export default { export default {
name: "configurationList", name: "configurationList",
components: {ConfigurationCard},
async setup(){ async setup(){
const wireguardConfigurationsStore = WireguardConfigurationsStore(); const wireguardConfigurationsStore = WireguardConfigurationsStore();
await wireguardConfigurationsStore.getConfigurations(); await wireguardConfigurationsStore.getConfigurations();
@ -25,24 +27,7 @@ export default {
<p class="text-muted" v-if="this.wireguardConfigurationsStore.Configurations.length === 0">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".</p> <p class="text-muted" v-if="this.wireguardConfigurationsStore.Configurations.length === 0">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".</p>
<div class="d-flex gap-3 flex-column" v-else > <div class="d-flex gap-3 flex-column" v-else >
<RouterLink :to="'/configuration/' + c.Name" <ConfigurationCard v-for="c in this.wireguardConfigurationsStore.Configurations" :key="c.Name" :c="c"></ConfigurationCard>
class="card conf_card rounded-3 shadow text-decoration-none" v-for="c in this.wireguardConfigurationsStore.Configurations" :key="c.Name">
<div class="card-body d-flex align-items-center gap-3 flex-wrap">
<h6 class="mb-0"><span class="dot" :class="{active: c.Status}"></span></h6>
<h6 class="card-title mb-0"><samp>{{c.Name}}</samp></h6>
<h6 class="mb-0 ms-auto">
<i class="bi bi-chevron-right"></i>
</h6>
</div>
<div class="card-footer">
<small class="me-2 text-muted">
<strong>PUBLIC KEY</strong>
</small>
<small class="mb-0 d-block d-lg-inline-block ">
<samp style="line-break: anywhere">{{c.PublicKey}}</samp>
</small>
</div>
</RouterLink>
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,71 @@
<script>
import {fetchGet} from "@/utilities/fetch.js";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
export default {
name: "configurationCard",
props: {
c: {
Name: String,
Status: Boolean,
PublicKey: String,
PrivateKey: String
}
},
setup(){
const dashboardConfigurationStore = DashboardConfigurationStore();
return {dashboardConfigurationStore}
},
methods: {
toggle(){
fetchGet("/api/toggleWireguardConfiguration/", {
configurationName: this.c.Name
}, (res) => {
if (res.status){
this.dashboardConfigurationStore.newMessage("Server",
`${this.c.Name} is ${res.data ? 'is on':'is off'}`)
}else{
this.dashboardConfigurationStore.newMessage("Server",
res.message, 'danger')
}
this.c.Status = res.data
})
}
}
}
</script>
<template>
<div class="card conf_card rounded-3 shadow text-decoration-none">
<RouterLink :to="'/configuration/' + c.Name" class="card-body d-flex align-items-center gap-3 flex-wrap text-decoration-none">
<h6 class="mb-0"><span class="dot" :class="{active: c.Status}"></span></h6>
<h6 class="card-title mb-0"><samp>{{c.Name}}</samp></h6>
<h6 class="mb-0 ms-auto">
<i class="bi bi-chevron-right"></i>
</h6>
</RouterLink>
<div class="card-footer d-flex align-items-center">
<small class="me-2 text-muted">
<strong>PUBLIC KEY</strong>
</small>
<small class="mb-0 d-block d-lg-inline-block ">
<samp style="line-break: anywhere">{{c.PublicKey}}</samp>
</small>
<div class="form-check form-switch ms-auto">
<label class="form-check-label" :for="'switch' + c.PrivateKey">
{{c.Status ? "On":"Off"}}
</label>
<input class="form-check-input"
type="checkbox" role="switch" :id="'switch' + c.PrivateKey"
@change="this.toggle()"
v-model="c.Status"
>
</div>
</div>
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,32 @@
<script>
export default {
name: "message",
props: {
message: Object
},
mounted() {
setTimeout(() => {
this.message.show = false
}, 5000)
}
}
</script>
<template>
<div class="card shadow rounded-3 position-relative mb-2"
:class="{
'text-bg-danger': this.message.type === 'danger',
'text-bg-success': this.message.type === 'success',
'text-bg-warning': this.message.type === 'warning'}"
:id="this.message.id"
style="width: 400px">
<div class="card-body">
<small class="fw-bold d-block" style="text-transform: uppercase">FROM {{this.message.from}}</small>
{{this.message.content}}
</div>
</div>
</template>
<style scoped>
</style>

View File

@ -10,6 +10,7 @@ import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStor
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js"; import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import Setup from "@/views/setup.vue"; import Setup from "@/views/setup.vue";
import NewConfiguration from "@/views/newConfiguration.vue"; import NewConfiguration from "@/views/newConfiguration.vue";
import Configuration from "@/views/configuration.vue";
const checkAuth = async () => { const checkAuth = async () => {
let result = false let result = false
@ -44,6 +45,11 @@ const router = createRouter({
name: "New Configuration", name: "New Configuration",
path: '/new_configuration', path: '/new_configuration',
component: NewConfiguration component: NewConfiguration
},
{
name: "Configuration",
path: '/configuration/:id',
component: Configuration
} }
] ]
}, },

View File

@ -1,10 +1,11 @@
import {defineStore} from "pinia"; import {defineStore} from "pinia";
import {fetchGet, fetchPost} from "@/utilities/fetch.js"; import {fetchGet, fetchPost} from "@/utilities/fetch.js";
import {cookie} from "@/utilities/cookie.js"; import {v4} from "uuid";
export const DashboardConfigurationStore = defineStore('DashboardConfigurationStore', { export const DashboardConfigurationStore = defineStore('DashboardConfigurationStore', {
state: () => ({ state: () => ({
Configuration: undefined Configuration: undefined,
Messages: []
}), }),
actions: { actions: {
async getConfiguration(){ async getConfiguration(){
@ -23,6 +24,15 @@ export const DashboardConfigurationStore = defineStore('DashboardConfigurationSt
await fetchGet("/api/signout", {}, (res) => { await fetchGet("/api/signout", {}, (res) => {
this.$router.go('/signin') this.$router.go('/signin')
}); });
},
newMessage(from, content, type){
this.Messages.push({
id: v4(),
from: from,
content: content,
type: type,
show: true
})
} }
} }
}); });

View File

@ -7,9 +7,7 @@ export const fetchGet = async (url, params=undefined, callback=undefined) => {
}) })
.then(x => x.json()) .then(x => x.json())
.then(x => callback ? callback(x) : undefined) .then(x => callback ? callback(x) : undefined)
.catch(() => {
alert("Error occurred! Check console")
});
} }
export const fetchPost = async (url, body, callback) => { export const fetchPost = async (url, body, callback) => {

View File

@ -0,0 +1,15 @@
<script>
export default {
name: "configuration"
}
</script>
<template>
<div class="text-body">
hiiii
</div>
</template>
<style scoped>
</style>

View File

@ -3,13 +3,19 @@ import Navbar from "@/components/navbar.vue";
import {wgdashboardStore} from "@/stores/wgdashboardStore.js"; import {wgdashboardStore} from "@/stores/wgdashboardStore.js";
import {WireguardConfigurations} from "@/models/WireguardConfigurations.js"; import {WireguardConfigurations} from "@/models/WireguardConfigurations.js";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js"; import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import Message from "@/components/messageCentreComponent/message.vue";
export default { export default {
name: "index", name: "index",
components: {Navbar}, components: {Message, Navbar},
async setup(){ async setup(){
const dashboardConfigurationStore = DashboardConfigurationStore() const dashboardConfigurationStore = DashboardConfigurationStore()
return {dashboardConfigurationStore} return {dashboardConfigurationStore}
},
computed: {
getMessages(){
return this.dashboardConfigurationStore.Messages.filter(x => x.show)
}
} }
} }
</script> </script>
@ -26,11 +32,20 @@ export default {
</Transition> </Transition>
</RouterView> </RouterView>
</Suspense> </Suspense>
<div class="messageCentre text-body position-fixed">
<TransitionGroup name="message" tag="div" class="position-relative">
<Message v-for="m in getMessages.slice().reverse()"
:message="m" :key="m.id"></Message>
</TransitionGroup>
</div>
</main> </main>
</div> </div>
</div> </div>
</template> </template>
<style scoped> <style scoped>
.messageCentre{
top: calc(50px + 1rem);
right: 1rem;
}
</style> </style>

View File

@ -2,6 +2,7 @@
import {parse} from "cidr-tools"; import {parse} from "cidr-tools";
import '@/utilities/wireguard.js' import '@/utilities/wireguard.js'
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js"; import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
import {fetchPost} from "@/utilities/fetch.js";
export default { export default {
name: "newConfiguration", name: "newConfiguration",
@ -21,10 +22,13 @@ export default {
PreUp: "", PreUp: "",
PreDown: "", PreDown: "",
PostUp: "", PostUp: "",
PostDown: "", PostDown: ""
UsePreSharedKey: false
}, },
numberOfAvailableIPs: "0" numberOfAvailableIPs: "0",
error: false,
errorMessage: "",
success: false,
loading: false
} }
}, },
created() { created() {
@ -36,14 +40,32 @@ export default {
this.newConfiguration.PrivateKey = wg.privateKey; this.newConfiguration.PrivateKey = wg.privateKey;
this.newConfiguration.PublicKey = wg.publicKey; this.newConfiguration.PublicKey = wg.publicKey;
this.newConfiguration.PresharedKey = wg.presharedKey; this.newConfiguration.PresharedKey = wg.presharedKey;
} },
async saveNewConfiguration(){
if (this.goodToSubmit){
this.loading = true;
await fetchPost("/api/addWireguardConfiguration", this.newConfiguration, async (res) => {
if (res.status){
this.success = true
await this.store.getConfigurations()
setTimeout(() => {
this.$router.push('/')
}, 1000)
}else{
this.error = true;
this.errorMessage = res.message;
document.querySelector(`#${res.data}`).classList.remove("is-valid")
document.querySelector(`#${res.data}`).classList.add("is-invalid")
}
})
}
}
}, },
computed: { computed: {
goodToSubmit(){ goodToSubmit(){
let requirements = ["ConfigurationName", "Address", "ListenPort", "PrivateKey"] let requirements = ["ConfigurationName", "Address", "ListenPort", "PrivateKey"]
let elements = [...document.querySelectorAll("input[required]")]; let elements = [...document.querySelectorAll("input[required]")];
return requirements.find(x => { return requirements.find(x => {
return this.newConfiguration[x].length === 0 return this.newConfiguration[x].length === 0
}) === undefined && elements.find(x => { }) === undefined && elements.find(x => {
@ -115,19 +137,26 @@ export default {
<h3 class="text-body mb-0">New Configuration</h3> <h3 class="text-body mb-0">New Configuration</h3>
</div> </div>
<form class="text-body d-flex flex-column gap-3"> <form class="text-body d-flex flex-column gap-3"
@submit="(e) => {e.preventDefault(); this.saveNewConfiguration();}"
>
<div class="card rounded-3 shadow"> <div class="card rounded-3 shadow">
<div class="card-header">Configuration Name</div> <div class="card-header">Configuration Name</div>
<div class="card-body"> <div class="card-body">
<input type="text" class="form-control" placeholder="ex. wg1" id="ConfigurationName" <input type="text" class="form-control" placeholder="ex. wg1" id="ConfigurationName"
v-model="this.newConfiguration.ConfigurationName" v-model="this.newConfiguration.ConfigurationName"
:disabled="this.loading"
required> required>
<div class="invalid-feedback"> <div class="invalid-feedback">
Configuration name is invalid. Possible reasons: <div v-if="this.error">{{this.errorMessage}}</div>
<ul class="mb-0"> <div v-else>
<li>Configuration name already exist.</li> Configuration name is invalid. Possible reasons:
<li>Configuration name can only contain 15 lower/uppercase alphabet, numbers, "_"(underscore), "="(equal), "+"(plus), "."(period/dot), "-"(dash/hyphen)</li> <ul class="mb-0">
</ul> <li>Configuration name already exist.</li>
<li>Configuration name can only contain 15 lower/uppercase alphabet, numbers, "_"(underscore), "="(equal), "+"(plus), "."(period/dot), "-"(dash/hyphen)</li>
</ul>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -138,6 +167,7 @@ export default {
<label class="text-muted fw-bold mb-1"><small>PRIVATE KEY</small></label> <label class="text-muted fw-bold mb-1"><small>PRIVATE KEY</small></label>
<div class="input-group"> <div class="input-group">
<input type="text" class="form-control" id="PrivateKey" required <input type="text" class="form-control" id="PrivateKey" required
:disabled="this.loading"
v-model="this.newConfiguration.PrivateKey" disabled v-model="this.newConfiguration.PrivateKey" disabled
> >
<button class="btn btn-outline-primary" type="button" <button class="btn btn-outline-primary" type="button"
@ -148,24 +178,13 @@ export default {
</button> </button>
</div> </div>
</div> </div>
<div class="row"> <div>
<div class="col-sm"> <label class="text-muted fw-bold mb-1"><small>PUBLIC KEY</small></label>
<label class="text-muted fw-bold mb-1"><small>PUBLIC KEY</small></label> <input type="text" class="form-control" id="PublicKey"
<input type="text" class="form-control" id="PublicKey" v-model="this.newConfiguration.PublicKey" disabled
v-model="this.newConfiguration.PublicKey" disabled >
>
</div>
<div class="col-sm" v-if="this.newConfiguration.UsePreSharedKey">
<label class="text-muted fw-bold mb-1"><small>PRE-SHARED KEY</small></label>
<input type="text" class="form-control" id="PresharedKey"
v-model="this.newConfiguration.PresharedKey" disabled
>
</div>
</div>
<div class="form-check form-switch mt-2">
<input class="form-check-input" type="checkbox" role="switch" id="UsePreSharedKey" v-model="this.newConfiguration.UsePreSharedKey">
<label class="form-check-label" for="UsePreSharedKey"><small>Use Pre-Shared Key?</small></label>
</div> </div>
</div> </div>
</div> </div>
@ -176,7 +195,14 @@ export default {
min="1" min="1"
max="65353" max="65353"
v-model="this.newConfiguration.ListenPort" v-model="this.newConfiguration.ListenPort"
:disabled="this.loading"
required> required>
<div class="invalid-feedback">
<div v-if="this.error">{{this.errorMessage}}</div>
<div v-else>
Invalid port
</div>
</div>
</div> </div>
</div> </div>
<div class="card rounded-3 shadow"> <div class="card rounded-3 shadow">
@ -188,9 +214,14 @@ export default {
<input type="text" class="form-control" <input type="text" class="form-control"
placeholder="Ex: 10.0.0.1/24" id="Address" placeholder="Ex: 10.0.0.1/24" id="Address"
v-model="this.newConfiguration.Address" v-model="this.newConfiguration.Address"
:disabled="this.loading"
required> required>
<div class="invalid-feedback"> <div class="invalid-feedback">
IP address & range is invalid. <div v-if="this.error">{{this.errorMessage}}</div>
<div v-else>
IP address & range is invalid.
</div>
</div> </div>
</div> </div>
</div> </div>
@ -237,10 +268,21 @@ export default {
<!-- <i class="bi bi-save me-2"></i>--> <!-- <i class="bi bi-save me-2"></i>-->
<!-- Save--> <!-- Save-->
<!-- </RouterLink>--> <!-- </RouterLink>-->
<button class="btn btn-dark btn-brand rounded-3 px-3 py-2 shadow ms-auto" :disabled="!this.goodToSubmit"> <button class="btn btn-dark btn-brand rounded-3 px-3 py-2 shadow ms-auto"
:disabled="!this.goodToSubmit">
<span v-if="this.success" class="d-flex w-100">
Success! <i class="bi bi-check-circle-fill ms-2"></i>
</span>
<span v-else-if="!this.loading" class="d-flex w-100">
Save Configuration <i class="bi bi-save-fill ms-2"></i>
</span>
<span v-else class="d-flex w-100 align-items-center">
Saving...
<span class="ms-2 spinner-border spinner-border-sm" role="status">
<!-- <span class="visually-hidden">Loading...</span>-->
</span>
</span>
Save Configuration
<i class="bi bi-save-fill ms-2"></i>
</button> </button>
</form> </form>

View File

@ -603,7 +603,6 @@ main {
.conf_card:hover { .conf_card:hover {
border-color: #007bff; border-color: #007bff;
cursor: pointer;
} }
.info_loading { .info_loading {
@ -1051,4 +1050,31 @@ pre.index-alert {
.totp{ .totp{
font-family: var(--bs-font-monospace); font-family: var(--bs-font-monospace);
}
.message-move, /* apply transition to moving elements */
.message-enter-active,
.message-leave-active {
transition: all 0.5s ease;
}
.message-enter-from,
.message-leave-to {
filter: blur(2px);
opacity: 0;
}
.message-enter-from{
transform: translateY(-30px) scale(0.7);
}
.message-leave-to{
transform: translateY(30px);
}
/* ensure leaving items are taken out of layout flow so that moving
animations can be calculated correctly. */
.message-leave-active {
position: absolute;
} }