mirror of
https://github.com/donaldzou/WGDashboard.git
synced 2024-11-22 15:20:09 +01:00
I am giving up on using ORM...
Lets go back to the good old sql query days ;)
This commit is contained in:
parent
6b6ad05e3a
commit
1e88491ca1
@ -1,3 +1,4 @@
|
||||
import itertools
|
||||
import sqlite3
|
||||
import configparser
|
||||
import hashlib
|
||||
@ -19,9 +20,9 @@ from operator import itemgetter
|
||||
from typing import Dict, Any
|
||||
|
||||
import bcrypt
|
||||
import flask
|
||||
# PIP installed library
|
||||
import ifcfg
|
||||
import psutil
|
||||
import pyotp
|
||||
from flask import Flask, request, render_template, redirect, url_for, session, jsonify, g
|
||||
from flask.json.provider import JSONProvider
|
||||
@ -57,7 +58,7 @@ QRcode(app)
|
||||
'''
|
||||
Classes
|
||||
'''
|
||||
Base = declarative_base()
|
||||
# Base = declarative_base(class_registry=dict())
|
||||
|
||||
|
||||
class CustomJsonEncoder(JSONProvider):
|
||||
@ -73,25 +74,40 @@ class CustomJsonEncoder(JSONProvider):
|
||||
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:
|
||||
__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):
|
||||
def __init__(self, m):
|
||||
self.message = m
|
||||
@ -99,10 +115,28 @@ class WireguardConfiguration:
|
||||
def __str__(self):
|
||||
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:
|
||||
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()
|
||||
if "Interface" not in sections:
|
||||
raise self.InvalidConfigurationFileException(
|
||||
@ -118,33 +152,195 @@ class WireguardConfiguration:
|
||||
if self.PrivateKey:
|
||||
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):
|
||||
inspector = inspect(engine)
|
||||
existingTable = inspector.get_table_names()
|
||||
if self.Name not in existingTable:
|
||||
_createPeerModel(self.Name).__table__.create(engine)
|
||||
if self.Name + "_restrict_access" not in existingTable:
|
||||
_createRestrcitedPeerModel(self.Name).__table__.create(engine)
|
||||
if self.Name + "_transfer" not in existingTable:
|
||||
_createPeerTransferModel(self.Name).__table__.create(engine)
|
||||
existingTables = cursor.execute("SELECT name FROM sqlite_master WHERE type='table'").fetchall()
|
||||
existingTables = itertools.chain(*existingTables)
|
||||
if self.Name not in existingTables:
|
||||
cursor.execute(
|
||||
"""
|
||||
CREATE TABLE %s (
|
||||
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}_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:
|
||||
return subprocess.check_output(['wg', 'pubkey'], input=self.PrivateKey.encode()).decode().strip('\n')
|
||||
|
||||
def __getStatus(self) -> bool:
|
||||
return self.Name in dict(ifcfg.interfaces().items()).keys()
|
||||
def getStatus(self) -> bool:
|
||||
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):
|
||||
self.Status = self.__getStatus()
|
||||
return self.__dict__
|
||||
self.Status = self.getStatus()
|
||||
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):
|
||||
@ -157,7 +353,7 @@ class DashboardConfig:
|
||||
|
||||
def __init__(self):
|
||||
self.__config = configparser.ConfigParser(strict=False)
|
||||
self.__config.read(DASHBOARD_CONF)
|
||||
self.__config.read_file(open(DASHBOARD_CONF))
|
||||
self.hiddenAttribute = ["totp_key"]
|
||||
self.__default = {
|
||||
"Account": {
|
||||
@ -309,7 +505,7 @@ def ResponseObject(status=True, message=None, data=None) -> Flask.response_class
|
||||
|
||||
|
||||
DashboardConfig = DashboardConfig()
|
||||
WireguardConfigurations: [WireguardConfiguration] = []
|
||||
WireguardConfigurations: {str: WireguardConfiguration} = {}
|
||||
|
||||
'''
|
||||
Private Functions
|
||||
@ -320,71 +516,71 @@ def _strToBool(value: str) -> bool:
|
||||
return value.lower() in ("yes", "true", "t", "1", 1)
|
||||
|
||||
|
||||
def _createPeerModel(wgConfigName):
|
||||
class Peer(Base):
|
||||
__tablename__ = wgConfigName
|
||||
id = mapped_column(VARCHAR, primary_key=True)
|
||||
private_key = mapped_column(VARCHAR)
|
||||
DNS = mapped_column(VARCHAR)
|
||||
endpoint_allowed_ip = mapped_column(VARCHAR)
|
||||
name = mapped_column(VARCHAR)
|
||||
total_receive = mapped_column(FLOAT)
|
||||
total_sent = mapped_column(FLOAT)
|
||||
total_data = mapped_column(FLOAT)
|
||||
endpoint = mapped_column(VARCHAR)
|
||||
status = mapped_column(VARCHAR)
|
||||
latest_handshake = mapped_column(VARCHAR)
|
||||
allowed_ip = mapped_column(VARCHAR)
|
||||
cumu_receive = mapped_column(FLOAT)
|
||||
cumu_sent = mapped_column(FLOAT)
|
||||
cumu_data = mapped_column(FLOAT)
|
||||
mtu = mapped_column(INT)
|
||||
keepalive = mapped_column(INT)
|
||||
remote_endpoint = mapped_column(VARCHAR)
|
||||
preshared_key = mapped_column(VARCHAR)
|
||||
|
||||
return Peer
|
||||
|
||||
|
||||
def _createRestrcitedPeerModel(wgConfigName):
|
||||
class PeerRestricted(Base):
|
||||
__tablename__ = wgConfigName + "_restrict_access"
|
||||
id = mapped_column(VARCHAR, primary_key=True)
|
||||
private_key = mapped_column(VARCHAR)
|
||||
DNS = mapped_column(VARCHAR)
|
||||
endpoint_allowed_ip = mapped_column(VARCHAR)
|
||||
name = mapped_column(VARCHAR)
|
||||
total_receive = mapped_column(FLOAT)
|
||||
total_sent = mapped_column(FLOAT)
|
||||
total_data = mapped_column(FLOAT)
|
||||
endpoint = mapped_column(VARCHAR)
|
||||
status = mapped_column(VARCHAR)
|
||||
latest_handshake = mapped_column(VARCHAR)
|
||||
allowed_ip = mapped_column(VARCHAR)
|
||||
cumu_receive = mapped_column(FLOAT)
|
||||
cumu_sent = mapped_column(FLOAT)
|
||||
cumu_data = mapped_column(FLOAT)
|
||||
mtu = mapped_column(INT)
|
||||
keepalive = mapped_column(INT)
|
||||
remote_endpoint = mapped_column(VARCHAR)
|
||||
preshared_key = mapped_column(VARCHAR)
|
||||
|
||||
return PeerRestricted
|
||||
|
||||
|
||||
def _createPeerTransferModel(wgConfigName):
|
||||
class PeerTransfer(Base):
|
||||
__tablename__ = wgConfigName + "_transfer"
|
||||
id = mapped_column(VARCHAR, primary_key=True)
|
||||
total_receive = mapped_column(FLOAT)
|
||||
total_sent = mapped_column(FLOAT)
|
||||
total_data = mapped_column(FLOAT)
|
||||
cumu_receive = mapped_column(FLOAT)
|
||||
cumu_sent = mapped_column(FLOAT)
|
||||
cumu_data = mapped_column(FLOAT)
|
||||
time = mapped_column(DATETIME)
|
||||
|
||||
return PeerTransfer
|
||||
# def _createPeerModel(wgConfigName):
|
||||
# return type(wgConfigName, (Base,), {
|
||||
# "id": mapped_column(VARCHAR, primary_key=True),
|
||||
# "private_key": mapped_column(VARCHAR),
|
||||
# "DNS": mapped_column(VARCHAR),
|
||||
# "endpoint_allowed_ip": mapped_column(VARCHAR),
|
||||
# "name": mapped_column(VARCHAR),
|
||||
# "total_receive": mapped_column(FLOAT),
|
||||
# "total_sent": mapped_column(FLOAT),
|
||||
# "total_data": mapped_column(FLOAT),
|
||||
# "endpoint": mapped_column(VARCHAR),
|
||||
# "status": mapped_column(VARCHAR),
|
||||
# "latest_handshake": mapped_column(VARCHAR),
|
||||
# "allowed_ip": mapped_column(VARCHAR),
|
||||
# "cumu_receive": mapped_column(FLOAT),
|
||||
# "cumu_sent": mapped_column(FLOAT),
|
||||
# "cumu_data": mapped_column(FLOAT),
|
||||
# "mtu": mapped_column(INT),
|
||||
# "keepalive": mapped_column(INT),
|
||||
# "remote_endpoint": mapped_column(VARCHAR),
|
||||
# "preshared_key": mapped_column(VARCHAR),
|
||||
# "__tablename__": wgConfigName,
|
||||
# "__table_args__": {'extend_existing': True}
|
||||
# })
|
||||
#
|
||||
#
|
||||
# def _createRestrictedPeerModel(wgConfigName):
|
||||
# return type(wgConfigName + "_restrict_access", (Base,), {
|
||||
# "id": mapped_column(VARCHAR, primary_key=True),
|
||||
# "private_key": mapped_column(VARCHAR),
|
||||
# "DNS": mapped_column(VARCHAR),
|
||||
# "endpoint_allowed_ip": mapped_column(VARCHAR),
|
||||
# "name": mapped_column(VARCHAR),
|
||||
# "total_receive": mapped_column(FLOAT),
|
||||
# "total_sent": mapped_column(FLOAT),
|
||||
# "total_data": mapped_column(FLOAT),
|
||||
# "endpoint": mapped_column(VARCHAR),
|
||||
# "status": mapped_column(VARCHAR),
|
||||
# "latest_handshake": mapped_column(VARCHAR),
|
||||
# "allowed_ip": mapped_column(VARCHAR),
|
||||
# "cumu_receive": mapped_column(FLOAT),
|
||||
# "cumu_sent": mapped_column(FLOAT),
|
||||
# "cumu_data": mapped_column(FLOAT),
|
||||
# "mtu": mapped_column(INT),
|
||||
# "keepalive": mapped_column(INT),
|
||||
# "remote_endpoint": mapped_column(VARCHAR),
|
||||
# "preshared_key": mapped_column(VARCHAR),
|
||||
# "__tablename__": wgConfigName,
|
||||
# "__table_args__": {'extend_existing': True}
|
||||
# })
|
||||
#
|
||||
#
|
||||
# def _createPeerTransferModel(wgConfigName):
|
||||
# return type(wgConfigName + "_transfer", (Base,), {
|
||||
# "id": mapped_column(VARCHAR, primary_key=True),
|
||||
# "total_receive": mapped_column(FLOAT),
|
||||
# "total_sent": mapped_column(FLOAT),
|
||||
# "total_data": mapped_column(FLOAT),
|
||||
# "cumu_receive": mapped_column(FLOAT),
|
||||
# "cumu_sent": mapped_column(FLOAT),
|
||||
# "cumu_data": mapped_column(FLOAT),
|
||||
# "time": mapped_column(DATETIME),
|
||||
# "__tablename__": wgConfigName + "_transfer",
|
||||
# "__table_args__": {'extend_existing': True},
|
||||
# })
|
||||
|
||||
|
||||
def _regexMatch(regex, text):
|
||||
@ -393,12 +589,12 @@ def _regexMatch(regex, text):
|
||||
|
||||
|
||||
def _getConfigurationList() -> [WireguardConfiguration]:
|
||||
configurations = []
|
||||
configurations = {}
|
||||
for i in os.listdir(WG_CONF_PATH):
|
||||
if _regexMatch("^(.{1,}).(conf)$", i):
|
||||
i = i.replace('.conf', '')
|
||||
try:
|
||||
configurations.append(WireguardConfiguration(i))
|
||||
configurations[i] = WireguardConfiguration(i)
|
||||
except WireguardConfiguration.InvalidConfigurationFileException as e:
|
||||
print(f"{i} have an invalid configuration file.")
|
||||
return configurations
|
||||
@ -471,7 +667,7 @@ def API_SignOut():
|
||||
@app.route('/api/getWireguardConfigurations', methods=["GET"])
|
||||
def API_getWireguardConfigurations():
|
||||
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"])
|
||||
@ -488,12 +684,46 @@ def API_addWireguardConfiguration():
|
||||
"PreDown",
|
||||
"PostUp",
|
||||
"PostDown",
|
||||
"UsePreSharedKey"
|
||||
]
|
||||
requiredKeys = [
|
||||
"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"])
|
||||
@ -590,11 +820,75 @@ def index():
|
||||
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__":
|
||||
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_port = DashboardConfig.GetConfig("Server", "app_port")
|
||||
_, WG_CONF_PATH = DashboardConfig.GetConfig("Server", "wg_conf_path")
|
||||
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)
|
||||
|
@ -1,9 +1,11 @@
|
||||
<script>
|
||||
import {wgdashboardStore} from "@/stores/wgdashboardStore.js";
|
||||
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
|
||||
import ConfigurationCard from "@/components/configurationListComponents/configurationCard.vue";
|
||||
|
||||
export default {
|
||||
name: "configurationList",
|
||||
components: {ConfigurationCard},
|
||||
async setup(){
|
||||
const wireguardConfigurationsStore = WireguardConfigurationsStore();
|
||||
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>
|
||||
|
||||
<div class="d-flex gap-3 flex-column" v-else >
|
||||
<RouterLink :to="'/configuration/' + c.Name"
|
||||
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>
|
||||
<ConfigurationCard v-for="c in this.wireguardConfigurationsStore.Configurations" :key="c.Name" :c="c"></ConfigurationCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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>
|
@ -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>
|
@ -10,6 +10,7 @@ import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStor
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
import Setup from "@/views/setup.vue";
|
||||
import NewConfiguration from "@/views/newConfiguration.vue";
|
||||
import Configuration from "@/views/configuration.vue";
|
||||
|
||||
const checkAuth = async () => {
|
||||
let result = false
|
||||
@ -44,6 +45,11 @@ const router = createRouter({
|
||||
name: "New Configuration",
|
||||
path: '/new_configuration',
|
||||
component: NewConfiguration
|
||||
},
|
||||
{
|
||||
name: "Configuration",
|
||||
path: '/configuration/:id',
|
||||
component: Configuration
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -1,10 +1,11 @@
|
||||
import {defineStore} from "pinia";
|
||||
import {fetchGet, fetchPost} from "@/utilities/fetch.js";
|
||||
import {cookie} from "@/utilities/cookie.js";
|
||||
import {v4} from "uuid";
|
||||
|
||||
export const DashboardConfigurationStore = defineStore('DashboardConfigurationStore', {
|
||||
state: () => ({
|
||||
Configuration: undefined
|
||||
Configuration: undefined,
|
||||
Messages: []
|
||||
}),
|
||||
actions: {
|
||||
async getConfiguration(){
|
||||
@ -23,6 +24,15 @@ export const DashboardConfigurationStore = defineStore('DashboardConfigurationSt
|
||||
await fetchGet("/api/signout", {}, (res) => {
|
||||
this.$router.go('/signin')
|
||||
});
|
||||
},
|
||||
newMessage(from, content, type){
|
||||
this.Messages.push({
|
||||
id: v4(),
|
||||
from: from,
|
||||
content: content,
|
||||
type: type,
|
||||
show: true
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
@ -7,9 +7,7 @@ export const fetchGet = async (url, params=undefined, callback=undefined) => {
|
||||
})
|
||||
.then(x => x.json())
|
||||
.then(x => callback ? callback(x) : undefined)
|
||||
.catch(() => {
|
||||
alert("Error occurred! Check console")
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
export const fetchPost = async (url, body, callback) => {
|
||||
|
15
src/static/app/src/views/configuration.vue
Normal file
15
src/static/app/src/views/configuration.vue
Normal file
@ -0,0 +1,15 @@
|
||||
<script>
|
||||
export default {
|
||||
name: "configuration"
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="text-body">
|
||||
hiiii
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@ -3,13 +3,19 @@ import Navbar from "@/components/navbar.vue";
|
||||
import {wgdashboardStore} from "@/stores/wgdashboardStore.js";
|
||||
import {WireguardConfigurations} from "@/models/WireguardConfigurations.js";
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
import Message from "@/components/messageCentreComponent/message.vue";
|
||||
|
||||
export default {
|
||||
name: "index",
|
||||
components: {Navbar},
|
||||
components: {Message, Navbar},
|
||||
async setup(){
|
||||
const dashboardConfigurationStore = DashboardConfigurationStore()
|
||||
return {dashboardConfigurationStore}
|
||||
},
|
||||
computed: {
|
||||
getMessages(){
|
||||
return this.dashboardConfigurationStore.Messages.filter(x => x.show)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -26,11 +32,20 @@ export default {
|
||||
</Transition>
|
||||
</RouterView>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.messageCentre{
|
||||
top: calc(50px + 1rem);
|
||||
right: 1rem;
|
||||
}
|
||||
</style>
|
@ -2,6 +2,7 @@
|
||||
import {parse} from "cidr-tools";
|
||||
import '@/utilities/wireguard.js'
|
||||
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
|
||||
import {fetchPost} from "@/utilities/fetch.js";
|
||||
|
||||
export default {
|
||||
name: "newConfiguration",
|
||||
@ -21,10 +22,13 @@ export default {
|
||||
PreUp: "",
|
||||
PreDown: "",
|
||||
PostUp: "",
|
||||
PostDown: "",
|
||||
UsePreSharedKey: false
|
||||
PostDown: ""
|
||||
},
|
||||
numberOfAvailableIPs: "0"
|
||||
numberOfAvailableIPs: "0",
|
||||
error: false,
|
||||
errorMessage: "",
|
||||
success: false,
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
created() {
|
||||
@ -36,14 +40,32 @@ export default {
|
||||
this.newConfiguration.PrivateKey = wg.privateKey;
|
||||
this.newConfiguration.PublicKey = wg.publicKey;
|
||||
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: {
|
||||
goodToSubmit(){
|
||||
let requirements = ["ConfigurationName", "Address", "ListenPort", "PrivateKey"]
|
||||
let elements = [...document.querySelectorAll("input[required]")];
|
||||
|
||||
|
||||
return requirements.find(x => {
|
||||
return this.newConfiguration[x].length === 0
|
||||
}) === undefined && elements.find(x => {
|
||||
@ -115,19 +137,26 @@ export default {
|
||||
<h3 class="text-body mb-0">New Configuration</h3>
|
||||
</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-header">Configuration Name</div>
|
||||
<div class="card-body">
|
||||
<input type="text" class="form-control" placeholder="ex. wg1" id="ConfigurationName"
|
||||
v-model="this.newConfiguration.ConfigurationName"
|
||||
:disabled="this.loading"
|
||||
required>
|
||||
<div class="invalid-feedback">
|
||||
Configuration name is invalid. Possible reasons:
|
||||
<ul class="mb-0">
|
||||
<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 v-if="this.error">{{this.errorMessage}}</div>
|
||||
<div v-else>
|
||||
Configuration name is invalid. Possible reasons:
|
||||
<ul class="mb-0">
|
||||
<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>
|
||||
@ -138,6 +167,7 @@ export default {
|
||||
<label class="text-muted fw-bold mb-1"><small>PRIVATE KEY</small></label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="PrivateKey" required
|
||||
:disabled="this.loading"
|
||||
v-model="this.newConfiguration.PrivateKey" disabled
|
||||
>
|
||||
<button class="btn btn-outline-primary" type="button"
|
||||
@ -148,24 +178,13 @@ export default {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<label class="text-muted fw-bold mb-1"><small>PUBLIC KEY</small></label>
|
||||
<input type="text" class="form-control" id="PublicKey"
|
||||
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>
|
||||
<label class="text-muted fw-bold mb-1"><small>PUBLIC KEY</small></label>
|
||||
<input type="text" class="form-control" id="PublicKey"
|
||||
v-model="this.newConfiguration.PublicKey" disabled
|
||||
>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@ -176,7 +195,14 @@ export default {
|
||||
min="1"
|
||||
max="65353"
|
||||
v-model="this.newConfiguration.ListenPort"
|
||||
:disabled="this.loading"
|
||||
required>
|
||||
<div class="invalid-feedback">
|
||||
<div v-if="this.error">{{this.errorMessage}}</div>
|
||||
<div v-else>
|
||||
Invalid port
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card rounded-3 shadow">
|
||||
@ -188,9 +214,14 @@ export default {
|
||||
<input type="text" class="form-control"
|
||||
placeholder="Ex: 10.0.0.1/24" id="Address"
|
||||
v-model="this.newConfiguration.Address"
|
||||
:disabled="this.loading"
|
||||
required>
|
||||
<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>
|
||||
@ -237,10 +268,21 @@ export default {
|
||||
<!-- <i class="bi bi-save me-2"></i>-->
|
||||
<!-- Save-->
|
||||
<!-- </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>
|
||||
</form>
|
||||
|
||||
|
@ -603,7 +603,6 @@ main {
|
||||
|
||||
.conf_card:hover {
|
||||
border-color: #007bff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.info_loading {
|
||||
@ -1052,3 +1051,30 @@ pre.index-alert {
|
||||
.totp{
|
||||
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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user