mirror of
https://github.com/donaldzou/WGDashboard.git
synced 2024-11-22 15:20:09 +01:00
Huge update
A welcome session Added Time based One-Time-Passcode (TOTP) UI Update
This commit is contained in:
parent
95a8867527
commit
5f4a364095
@ -18,9 +18,11 @@ from json import JSONEncoder
|
||||
from operator import itemgetter
|
||||
from typing import Dict, Any
|
||||
|
||||
import bcrypt
|
||||
import flask
|
||||
# PIP installed library
|
||||
import ifcfg
|
||||
import pyotp
|
||||
from flask import Flask, request, render_template, redirect, url_for, session, jsonify, g
|
||||
from flask.json.provider import JSONProvider
|
||||
from flask_qrcode import QRcode
|
||||
@ -138,15 +140,24 @@ class WireguardConfiguration:
|
||||
return self.__dict__
|
||||
|
||||
|
||||
def iPv46RegexCheck(ip):
|
||||
return re.match(
|
||||
'((^\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9a-f]{1,4}:){7}([0-9a-f]{1,4}|:))|(([0-9a-f]{1,4}:){6}(:[0-9a-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){5}(((:[0-9a-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){4}(((:[0-9a-f]{1,4}){1,3})|((:[0-9a-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){3}(((:[0-9a-f]{1,4}){1,4})|((:[0-9a-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){2}(((:[0-9a-f]{1,4}){1,5})|((:[0-9a-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){1}(((:[0-9a-f]{1,4}){1,6})|((:[0-9a-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9a-f]{1,4}){1,7})|((:[0-9a-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$))',
|
||||
ip)
|
||||
|
||||
|
||||
class DashboardConfig:
|
||||
|
||||
def __init__(self):
|
||||
self.__config = configparser.ConfigParser(strict=False)
|
||||
self.__config.read(DASHBOARD_CONF)
|
||||
self.hiddenAttribute = ["totp_key"]
|
||||
self.__default = {
|
||||
"Account": {
|
||||
"username": "admin",
|
||||
"password": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918"
|
||||
"password": "admin",
|
||||
"enable_totp": "false",
|
||||
"totp_key": pyotp.random_base32()
|
||||
},
|
||||
"Server": {
|
||||
"wg_conf_path": "/etc/wireguard",
|
||||
@ -156,7 +167,7 @@ class DashboardConfig:
|
||||
"version": DASHBOARD_VERSION,
|
||||
"dashboard_refresh_interval": "60000",
|
||||
"dashboard_sort": "status",
|
||||
"dashboard_theme": "light"
|
||||
"dashboard_theme": "dark"
|
||||
},
|
||||
"Peers": {
|
||||
"peer_global_DNS": "1.1.1.1",
|
||||
@ -165,6 +176,9 @@ class DashboardConfig:
|
||||
"remote_endpoint": ifcfg.default_interface()['inet'],
|
||||
"peer_MTU": "1420",
|
||||
"peer_keep_alive": "21"
|
||||
},
|
||||
"Other": {
|
||||
"welcome_session": "true"
|
||||
}
|
||||
}
|
||||
|
||||
@ -172,13 +186,71 @@ class DashboardConfig:
|
||||
for key, value in keys.items():
|
||||
exist, currentData = self.GetConfig(section, key)
|
||||
if not exist:
|
||||
self.SetConfig(section, key, value)
|
||||
self.SetConfig(section, key, value, True)
|
||||
|
||||
def __configValidation(self, key, value: Any) -> [bool, str]:
|
||||
if type(value) is str and len(value) == 0:
|
||||
return False, "Field cannot be empty!"
|
||||
if key == "peer_global_dns":
|
||||
value = value.split(",")
|
||||
for i in value:
|
||||
try:
|
||||
ipaddress.ip_address(i)
|
||||
except ValueError as e:
|
||||
return False, str(e)
|
||||
if key == "peer_endpoint_allowed_ip":
|
||||
value = value.split(",")
|
||||
for i in value:
|
||||
try:
|
||||
ipaddress.ip_network(i, strict=False)
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
if key == "wg_conf_path":
|
||||
if not os.path.exists(value):
|
||||
return False, f"{value} is not a valid path"
|
||||
if key == "password":
|
||||
if self.GetConfig("Account", "password")[0]:
|
||||
if not self.__checkPassword(
|
||||
value["currentPassword"], self.GetConfig("Account", "password")[1].encode("utf-8")):
|
||||
return False, "Current password does not match."
|
||||
if value["newPassword"] != value["repeatNewPassword"]:
|
||||
return False, "New passwords does not match"
|
||||
return True, ""
|
||||
|
||||
def generatePassword(self, plainTextPassword: str):
|
||||
return bcrypt.hashpw(plainTextPassword.encode("utf-8"), bcrypt.gensalt(rounds=12))
|
||||
|
||||
def __checkPassword(self, plainTextPassword: str, hashedPassword: bytes):
|
||||
return bcrypt.checkpw(plainTextPassword.encode("utf-8"), hashedPassword)
|
||||
|
||||
def SetConfig(self, section: str, key: str, value: any, init: bool = False) -> [bool, str]:
|
||||
if key in self.hiddenAttribute and not init:
|
||||
return False, None
|
||||
|
||||
if not init:
|
||||
valid, msg = self.__configValidation(key, value)
|
||||
if not valid:
|
||||
return False, msg
|
||||
|
||||
if section == "Account" and key == "password":
|
||||
if not init:
|
||||
value = self.generatePassword(value["newPassword"]).decode("utf-8")
|
||||
else:
|
||||
value = self.generatePassword(value).decode("utf-8")
|
||||
|
||||
def SetConfig(self, section: str, key: str, value: any) -> bool:
|
||||
if section not in self.__config:
|
||||
self.__config[section] = {}
|
||||
self.__config[section][key] = value
|
||||
return self.SaveConfig()
|
||||
|
||||
if key not in self.__config[section].keys() or value != self.__config[section][key]:
|
||||
if type(value) is bool:
|
||||
if value:
|
||||
self.__config[section][key] = "true"
|
||||
else:
|
||||
self.__config[section][key] = "false"
|
||||
else:
|
||||
self.__config[section][key] = value
|
||||
return self.SaveConfig(), ""
|
||||
return True, ""
|
||||
|
||||
def SaveConfig(self) -> bool:
|
||||
try:
|
||||
@ -195,14 +267,27 @@ class DashboardConfig:
|
||||
if key not in self.__config[section]:
|
||||
return False, None
|
||||
|
||||
if self.__config[section][key] in ["1", "yes", "true", "on"]:
|
||||
return True, True
|
||||
|
||||
if self.__config[section][key] in ["0", "no", "false", "off"]:
|
||||
return True, False
|
||||
|
||||
return True, self.__config[section][key]
|
||||
|
||||
def toJSON(self) -> dict[str, dict[Any, Any]]:
|
||||
the_dict = {}
|
||||
|
||||
for section in self.__config.sections():
|
||||
the_dict[section] = {}
|
||||
for key, val in self.__config.items(section):
|
||||
the_dict[section][key] = val
|
||||
if key not in self.hiddenAttribute:
|
||||
if val in ["1", "yes", "true", "on"]:
|
||||
the_dict[section][key] = True
|
||||
elif val in ["0", "no", "false", "off"]:
|
||||
the_dict[section][key] = False
|
||||
else:
|
||||
the_dict[section][key] = val
|
||||
return the_dict
|
||||
|
||||
|
||||
@ -319,10 +404,14 @@ API Routes
|
||||
|
||||
@app.before_request
|
||||
def auth_req():
|
||||
authenticationRequired = _strToBool(DashboardConfig.GetConfig("Server", "auth_req")[1])
|
||||
authenticationRequired = DashboardConfig.GetConfig("Server", "auth_req")[1]
|
||||
if authenticationRequired:
|
||||
|
||||
if ('/static/' not in request.path and "username" not in session and "/" != request.path
|
||||
and "validateAuthentication" not in request.path and "authenticate" not in request.path):
|
||||
and "validateAuthentication" not in request.path and "authenticate" not in request.path
|
||||
and "getDashboardConfiguration" not in request.path and "getDashboardTheme" not in request.path
|
||||
and "isTotpEnabled" not in request.path
|
||||
):
|
||||
resp = Flask.make_response(app, "Not Authorized" + request.path)
|
||||
resp.status_code = 401
|
||||
return resp
|
||||
@ -340,17 +429,32 @@ def API_ValidateAuthentication():
|
||||
@app.route('/api/authenticate', methods=['POST'])
|
||||
def API_AuthenticateLogin():
|
||||
data = request.get_json()
|
||||
password = hashlib.sha256(data['password'].encode())
|
||||
print()
|
||||
if password.hexdigest() == DashboardConfig.GetConfig("Account", "password")[1] \
|
||||
and data['username'] == DashboardConfig.GetConfig("Account", "username")[1]:
|
||||
valid = bcrypt.checkpw(data['password'].encode("utf-8"),
|
||||
DashboardConfig.GetConfig("Account", "password")[1].encode("utf-8"))
|
||||
totpEnabled = DashboardConfig.GetConfig("Account", "enable_totp")[1]
|
||||
totpValid = False
|
||||
|
||||
if totpEnabled:
|
||||
totpValid = pyotp.TOTP(DashboardConfig.GetConfig("Account", "totp_key")[1]).now() == data['totp']
|
||||
|
||||
if (valid
|
||||
and data['username'] == DashboardConfig.GetConfig("Account", "username")[1]
|
||||
and ((totpEnabled and totpValid) or not totpEnabled)
|
||||
):
|
||||
authToken = hashlib.sha256(f"{data['username']}{datetime.now()}".encode()).hexdigest()
|
||||
session['username'] = authToken
|
||||
resp = ResponseObject(True, "")
|
||||
resp = ResponseObject(True, DashboardConfig.GetConfig("Other", "welcome_session")[1])
|
||||
resp.set_cookie("authToken", authToken)
|
||||
session.permanent = True
|
||||
return resp
|
||||
return ResponseObject(False, "Username or password is incorrect.")
|
||||
return ResponseObject(False, "Username, password or OTP is incorrect.")
|
||||
|
||||
|
||||
@app.route('/api/signout')
|
||||
def API_SignOut():
|
||||
resp = ResponseObject(True, "")
|
||||
resp.delete_cookie("authToken")
|
||||
return resp
|
||||
|
||||
|
||||
@app.route('/api/getWireguardConfigurations', methods=["GET"])
|
||||
@ -364,6 +468,95 @@ def API_getDashboardConfiguration():
|
||||
return ResponseObject(data=DashboardConfig.toJSON())
|
||||
|
||||
|
||||
@app.route('/api/updateDashboardConfiguration', methods=["POST"])
|
||||
def API_updateDashboardConfiguration():
|
||||
data = request.get_json()
|
||||
for section in data['DashboardConfiguration'].keys():
|
||||
for key in data['DashboardConfiguration'][section].keys():
|
||||
if not DashboardConfig.SetConfig(section, key, data['DashboardConfiguration'][section][key])[0]:
|
||||
return ResponseObject(False, "Section or value is invalid.")
|
||||
return ResponseObject()
|
||||
|
||||
|
||||
@app.route('/api/updateDashboardConfigurationItem', methods=["POST"])
|
||||
def API_updateDashboardConfigurationItem():
|
||||
data = request.get_json()
|
||||
if "section" not in data.keys() or "key" not in data.keys() or "value" not in data.keys():
|
||||
return ResponseObject(False, "Invalid request.")
|
||||
|
||||
valid, msg = DashboardConfig.SetConfig(
|
||||
data["section"], data["key"], data['value'])
|
||||
|
||||
if not valid:
|
||||
return ResponseObject(False, msg)
|
||||
|
||||
return ResponseObject()
|
||||
|
||||
|
||||
@app.route('/api/getDashboardTheme')
|
||||
def API_getDashboardTheme():
|
||||
return ResponseObject(data=DashboardConfig.GetConfig("Server", "dashboard_theme")[1])
|
||||
|
||||
|
||||
@app.route('/api/isTotpEnabled')
|
||||
def API_isTotpEnabled():
|
||||
return ResponseObject(data=DashboardConfig.GetConfig("Account", "enable_totp")[1])
|
||||
|
||||
|
||||
@app.route('/api/Welcome_GetTotpLink')
|
||||
def API_Welcome_GetTotpLink():
|
||||
if DashboardConfig.GetConfig("Other", "welcome_session")[1]:
|
||||
return ResponseObject(
|
||||
data=pyotp.totp.TOTP(DashboardConfig.GetConfig("Account", "totp_key")[1]).provisioning_uri(
|
||||
issuer_name="WGDashboard"))
|
||||
return ResponseObject(False)
|
||||
|
||||
|
||||
@app.route('/api/Welcome_VerifyTotpLink', methods=["POST"])
|
||||
def API_Welcome_VerifyTotpLink():
|
||||
data = request.get_json()
|
||||
if DashboardConfig.GetConfig("Other", "welcome_session")[1]:
|
||||
return ResponseObject(pyotp.TOTP(DashboardConfig.GetConfig("Account", "totp_key")[1]).now() == data['totp'])
|
||||
return ResponseObject(False)
|
||||
|
||||
|
||||
@app.route('/api/Welcome_Finish', methods=["POST"])
|
||||
def API_Welcome_Finish():
|
||||
data = request.get_json()
|
||||
if DashboardConfig.GetConfig("Other", "welcome_session")[1]:
|
||||
if data["username"] == "":
|
||||
return ResponseObject(False, "Username cannot be blank.")
|
||||
|
||||
if data["newPassword"] == "" or len(data["newPassword"]) < 8:
|
||||
return ResponseObject(False, "Password must be at least 8 characters")
|
||||
|
||||
updateUsername, updateUsernameErr = DashboardConfig.SetConfig("Account", "username", data["username"])
|
||||
updatePassword, updatePasswordErr = DashboardConfig.SetConfig("Account", "password",
|
||||
{
|
||||
"newPassword": data["newPassword"],
|
||||
"repeatNewPassword": data[
|
||||
"repeatNewPassword"],
|
||||
"currentPassword": "admin"
|
||||
})
|
||||
updateEnableTotp, updateEnableTotpErr = DashboardConfig.SetConfig("Account", "enable_totp", data["enable_totp"])
|
||||
|
||||
if not updateUsername or not updatePassword or not updateEnableTotp:
|
||||
return ResponseObject(False, f"{updateUsernameErr},{updatePasswordErr},{updateEnableTotpErr}".strip(","))
|
||||
|
||||
DashboardConfig.SetConfig("Other", "welcome_session", False)
|
||||
|
||||
return ResponseObject()
|
||||
|
||||
|
||||
@app.route('/', methods=['GET'])
|
||||
def index():
|
||||
"""
|
||||
Index page related
|
||||
@return: Template
|
||||
"""
|
||||
return render_template('index_new.html')
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
engine = create_engine("sqlite:///" + os.path.join(CONFIGURATION_PATH, 'db', 'wgdashboard.db'))
|
||||
|
||||
@ -371,5 +564,4 @@ if __name__ == "__main__":
|
||||
_, app_port = DashboardConfig.GetConfig("Server", "app_port")
|
||||
_, WG_CONF_PATH = DashboardConfig.GetConfig("Server", "wg_conf_path")
|
||||
WireguardConfigurations = _getConfigurationList()
|
||||
|
||||
app.run(host=app_ip, debug=False, port=app_port)
|
||||
app.run(host=app_ip, debug=True, port=app_port)
|
||||
|
@ -7,7 +7,7 @@
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" class="vw-100 vh-100"></div>
|
||||
<div id="app" class="w-100 vh-100"></div>
|
||||
<script type="module" src="./src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
296
src/static/app/package-lock.json
generated
296
src/static/app/package-lock.json
generated
@ -11,6 +11,8 @@
|
||||
"bootstrap": "^5.3.2",
|
||||
"bootstrap-icons": "^1.11.2",
|
||||
"pinia": "^2.1.7",
|
||||
"qrcode": "^1.5.3",
|
||||
"uuid": "^9.0.1",
|
||||
"vue": "^3.3.11",
|
||||
"vue-router": "^4.2.5"
|
||||
},
|
||||
@ -690,6 +692,28 @@
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.3.tgz",
|
||||
"integrity": "sha512-rIwlkkP1n4uKrRzivAKPZIEkHiuwY5mmhMJ2nZKCBLz8lTUlE73rQh4n1OnnMurXt1vcUNyH4ZPfdh8QweTjpQ=="
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/bootstrap": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.2.tgz",
|
||||
@ -723,11 +747,68 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/camelcase": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
|
||||
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"wrap-ansi": "^6.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
|
||||
},
|
||||
"node_modules/decamelize": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dijkstrajs": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
|
||||
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
||||
},
|
||||
"node_modules/encode-utf8": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/encode-utf8/-/encode-utf8-1.0.3.tgz",
|
||||
"integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw=="
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
@ -782,6 +863,18 @@
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
|
||||
},
|
||||
"node_modules/find-up": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||
"dependencies": {
|
||||
"locate-path": "^5.0.0",
|
||||
"path-exists": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
@ -796,6 +889,33 @@
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||
"dependencies": {
|
||||
"p-locate": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.5",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz",
|
||||
@ -824,6 +944,47 @@
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/p-limit": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||
"dependencies": {
|
||||
"p-try": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-locate": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
|
||||
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||
"dependencies": {
|
||||
"p-limit": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/p-try": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||
@ -879,6 +1040,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/pngjs": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
|
||||
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.32",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz",
|
||||
@ -906,6 +1075,36 @@
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode": {
|
||||
"version": "1.5.3",
|
||||
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.3.tgz",
|
||||
"integrity": "sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==",
|
||||
"dependencies": {
|
||||
"dijkstrajs": "^1.0.1",
|
||||
"encode-utf8": "^1.0.3",
|
||||
"pngjs": "^5.0.0",
|
||||
"yargs": "^15.3.1"
|
||||
},
|
||||
"bin": {
|
||||
"qrcode": "bin/qrcode"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-main-filename": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.9.2",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.2.tgz",
|
||||
@ -935,6 +1134,11 @@
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
|
||||
@ -943,6 +1147,42 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
|
||||
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.0.10",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.0.10.tgz",
|
||||
@ -1031,6 +1271,62 @@
|
||||
"peerDependencies": {
|
||||
"vue": "^3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/which-module": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
|
||||
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
||||
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "15.4.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
||||
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
|
||||
"dependencies": {
|
||||
"cliui": "^6.0.0",
|
||||
"decamelize": "^1.2.0",
|
||||
"find-up": "^4.1.0",
|
||||
"get-caller-file": "^2.0.1",
|
||||
"require-directory": "^2.1.1",
|
||||
"require-main-filename": "^2.0.0",
|
||||
"set-blocking": "^2.0.0",
|
||||
"string-width": "^4.2.0",
|
||||
"which-module": "^2.0.0",
|
||||
"y18n": "^4.0.0",
|
||||
"yargs-parser": "^18.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs-parser": {
|
||||
"version": "18.1.3",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
|
||||
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
|
||||
"dependencies": {
|
||||
"camelcase": "^5.0.0",
|
||||
"decamelize": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,8 @@
|
||||
"bootstrap": "^5.3.2",
|
||||
"bootstrap-icons": "^1.11.2",
|
||||
"pinia": "^2.1.7",
|
||||
"qrcode": "^1.5.3",
|
||||
"uuid": "^9.0.1",
|
||||
"vue": "^3.3.11",
|
||||
"vue-router": "^4.2.5"
|
||||
},
|
||||
|
@ -17,6 +17,10 @@ body {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.dashboardLogo{
|
||||
background: -webkit-linear-gradient(#178bff, #ff4a00);
|
||||
}
|
||||
|
||||
/*
|
||||
* Sidebar
|
||||
*/
|
||||
|
@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
<script setup >
|
||||
import { RouterView } from 'vue-router'
|
||||
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
const store = DashboardConfigurationStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -9,5 +10,11 @@ import { RouterView } from 'vue-router'
|
||||
<span class="navbar-brand mb-0 h1">WGDashboard</span>
|
||||
</div>
|
||||
</nav>
|
||||
<RouterView></RouterView>
|
||||
<Suspense>
|
||||
<RouterView v-slot="{ Component }">
|
||||
<Transition name="fade" mode="out-in">
|
||||
<Component :is="Component"></Component>
|
||||
</Transition>
|
||||
</RouterView>
|
||||
</Suspense>
|
||||
</template>
|
@ -1,46 +1,48 @@
|
||||
<script>
|
||||
import {wgdashboardStore} from "@/stores/wgdashboardStore.js";
|
||||
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
|
||||
|
||||
export default {
|
||||
name: "configurationList",
|
||||
async setup(){
|
||||
const store = wgdashboardStore();
|
||||
await store.getWireguardConfigurations();
|
||||
return {store}
|
||||
const wireguardConfigurationsStore = WireguardConfigurationsStore();
|
||||
await wireguardConfigurationsStore.getConfigurations();
|
||||
return {wireguardConfigurationsStore}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mt-4">
|
||||
<h3 class="mb-3 text-body">Wireguard Configurations</h3>
|
||||
<p class="text-muted" v-if="this.store.WireguardConfigurations.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="card conf_card rounded-3 shadow" v-else v-for="c in this.store.WireguardConfigurations" :key="c.Name">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="row">
|
||||
<div class="col card-col">
|
||||
<small class="text-muted"><strong>CONFIGURATION</strong></small>
|
||||
<h6 class="card-title" style="margin:0 !important;"><samp>{{c.Name}}</samp></h6>
|
||||
</div>
|
||||
<div class="col card-col">
|
||||
<small class="text-muted"><strong>STATUS</strong></small>
|
||||
<h6><span>{{c.Status ? "Running":"Stopped"}}</span>
|
||||
<span class="dot" :class="{active: c.Status}"></span></h6>
|
||||
</div>
|
||||
<div class="col-sm card-col">
|
||||
<small class="text-muted"><strong>PUBLIC KEY</strong></small>
|
||||
<h6 style="margin:0 !important;"><samp>{{c.PublicKey}}</samp></h6>
|
||||
</div>
|
||||
<div class="col-sm index-switch">
|
||||
<div class="switch-test">
|
||||
<input type="checkbox" class="toggle--switch" checked :id="c.Name + '-switch'">
|
||||
<label :for="c.Name + '-switch'" class="toggleLabel"></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="d-flex mb-4 ">
|
||||
<h3 class="text-body">Wireguard Configurations</h3>
|
||||
<RouterLink to="/new_configuration" class="btn btn-dark btn-brand rounded-3 px-3 py-2 shadow ms-auto rounded-5">
|
||||
<i class="bi bi-plus-circle-fill me-2"></i>
|
||||
Configuration
|
||||
</RouterLink>
|
||||
</div>
|
||||
<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>
|
||||
<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>
|
||||
|
@ -1,50 +1,59 @@
|
||||
<script>
|
||||
import {wgdashboardStore} from "@/stores/wgdashboardStore.js";
|
||||
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
|
||||
export default {
|
||||
name: "navbar",
|
||||
setup(){
|
||||
const store = wgdashboardStore()
|
||||
return {store}
|
||||
const wireguardConfigurationsStore = WireguardConfigurationsStore();
|
||||
const dashboardConfigurationStore = DashboardConfigurationStore();
|
||||
return {wireguardConfigurationsStore, dashboardConfigurationStore}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav id="sidebarMenu" class="col-md-3 col-lg-2 d-md-block bg-body-tertiary sidebar border border-right p-0">
|
||||
<div class="sidebar-sticky pt-3">
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<RouterLink class="nav-link" to="/" exact-active-class="active">Home</RouterLink></li>
|
||||
<li class="nav-item">
|
||||
<RouterLink class="nav-link" to="/settings" exact-active-class="active">Settings</RouterLink></li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
|
||||
<span>Configurations</span>
|
||||
|
||||
</h6>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<RouterLink :to="'/configuration/'+c.Name" class="nav-link nav-conf-link" v-for="c in this.store.WireguardConfigurations">
|
||||
<samp>{{c.Name}}</samp>
|
||||
</RouterLink>
|
||||
</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
|
||||
<span>Tools</span>
|
||||
</h6>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item"><a class="nav-link" data-toggle="modal" data-target="#ping_modal" href="#">Ping</a></li>
|
||||
<li class="nav-item"><a class="nav-link" data-toggle="modal" data-target="#traceroute_modal" href="#">Traceroute</a></li>
|
||||
</ul>
|
||||
<hr>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item"><a href="https://github.com/donaldzou/WGDashboard/releases/tag/"><small class="nav-link text-muted"></small></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="col-md-3 col-lg-2 d-md-block p-3">
|
||||
<nav id="sidebarMenu" class=" bg-body-tertiary sidebar border h-100 rounded-3 shadow" >
|
||||
<div class="sidebar-sticky pt-3">
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<RouterLink class="nav-link" to="/" exact-active-class="active">Home</RouterLink></li>
|
||||
<li class="nav-item">
|
||||
<RouterLink class="nav-link" to="/settings" exact-active-class="active">Settings</RouterLink></li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
|
||||
<span>Configurations</span>
|
||||
</h6>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<RouterLink :to="'/configuration/'+c.Name" class="nav-link nav-conf-link" v-for="c in this.wireguardConfigurationsStore.Configurations">
|
||||
<samp>{{c.Name}}</samp>
|
||||
</RouterLink>
|
||||
</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
|
||||
<span>Tools</span>
|
||||
</h6>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item"><a class="nav-link" data-toggle="modal" data-target="#ping_modal" href="#">Ping</a></li>
|
||||
<li class="nav-item"><a class="nav-link" data-toggle="modal" data-target="#traceroute_modal" href="#">Traceroute</a></li>
|
||||
</ul>
|
||||
<hr>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item"><a class="nav-link text-danger" @click="this.dashboardConfigurationStore.signOut()" role="button" style="font-weight: bold">Sign Out</a></li>
|
||||
</ul>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item"><a href="https://github.com/donaldzou/WGDashboard/releases/tag/"><small class="nav-link text-muted"></small></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
@ -0,0 +1,120 @@
|
||||
<script>
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
import {v4} from "uuid";
|
||||
import {fetchPost} from "@/utilities/fetch.js";
|
||||
|
||||
export default {
|
||||
name: "accountSettingsInputPassword",
|
||||
props:{
|
||||
targetData: String,
|
||||
warning: false,
|
||||
warningText: ""
|
||||
},
|
||||
setup(){
|
||||
const store = DashboardConfigurationStore();
|
||||
const uuid = `input_${v4()}`;
|
||||
return {store, uuid};
|
||||
},
|
||||
data(){
|
||||
return{
|
||||
value: {
|
||||
currentPassword: "",
|
||||
newPassword: "",
|
||||
repeatNewPassword: ""
|
||||
},
|
||||
invalidFeedback: "",
|
||||
showInvalidFeedback: false,
|
||||
isValid: false,
|
||||
timeout: undefined
|
||||
}
|
||||
},
|
||||
methods:{
|
||||
async useValidation(){
|
||||
if (Object.values(this.value).find(x => x.length === 0) === undefined){
|
||||
if (this.value.newPassword === this.value.repeatNewPassword){
|
||||
await fetchPost("/api/updateDashboardConfigurationItem", {
|
||||
section: "Account",
|
||||
key: this.targetData,
|
||||
value: this.value
|
||||
}, (res) => {
|
||||
if (res.status){
|
||||
this.isValid = true;
|
||||
this.showInvalidFeedback = false;
|
||||
this.store.Configuration.Account[this.targetData] = this.value
|
||||
clearTimeout(this.timeout)
|
||||
this.timeout = setTimeout(() => {
|
||||
this.isValid = false;
|
||||
this.value = {
|
||||
currentPassword: "",
|
||||
newPassword: "",
|
||||
repeatNewPassword: ""
|
||||
}
|
||||
}, 5000);
|
||||
}else{
|
||||
this.isValid = false;
|
||||
this.showInvalidFeedback = true;
|
||||
this.invalidFeedback = res.message
|
||||
}
|
||||
})
|
||||
}else{
|
||||
this.showInvalidFeedback = true;
|
||||
this.invalidFeedback = "New passwords does not match"
|
||||
}
|
||||
|
||||
}else{
|
||||
this.showInvalidFeedback = true;
|
||||
this.invalidFeedback = "Please fill in all required fields."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group mb-2">
|
||||
<label :for="'currentPassword_' + this.uuid" class="text-muted mb-1">
|
||||
<strong><small>Current Password</small></strong>
|
||||
</label>
|
||||
<input type="password" class="form-control mb-2"
|
||||
:class="{'is-invalid': showInvalidFeedback, 'is-valid': isValid}"
|
||||
v-model="this.value.currentPassword"
|
||||
:id="'currentPassword_' + this.uuid">
|
||||
<div class="invalid-feedback d-block" v-if="showInvalidFeedback">{{this.invalidFeedback}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="form-group mb-2">
|
||||
<label :for="'newPassword_' + this.uuid" class="text-muted mb-1">
|
||||
<strong><small>New Password</small></strong>
|
||||
</label>
|
||||
<input type="password" class="form-control mb-2"
|
||||
:class="{'is-invalid': showInvalidFeedback, 'is-valid': isValid}"
|
||||
v-model="this.value.newPassword"
|
||||
:id="'newPassword_' + this.uuid">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="form-group mb-2">
|
||||
<label :for="'repeatNewPassword_' + this.uuid" class="text-muted mb-1">
|
||||
<strong><small>Repeat New Password</small></strong>
|
||||
</label>
|
||||
<input type="password" class="form-control mb-2"
|
||||
:class="{'is-invalid': showInvalidFeedback, 'is-valid': isValid}"
|
||||
v-model="this.value.repeatNewPassword"
|
||||
:id="'repeatNewPassword_' + this.uuid">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-success btn-sm fw-bold rounded-3" @click="this.useValidation()">
|
||||
<i class="bi bi-key-fill me-2"></i>Update Password
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@ -0,0 +1,86 @@
|
||||
<script>
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
import {v4} from "uuid";
|
||||
import {fetchPost} from "@/utilities/fetch.js";
|
||||
|
||||
export default {
|
||||
name: "accountSettingsInputUsername",
|
||||
props:{
|
||||
targetData: String,
|
||||
title: String,
|
||||
warning: false,
|
||||
warningText: ""
|
||||
},
|
||||
setup(){
|
||||
const store = DashboardConfigurationStore();
|
||||
const uuid = `input_${v4()}`;
|
||||
return {store, uuid};
|
||||
},
|
||||
data(){
|
||||
return{
|
||||
value:"",
|
||||
invalidFeedback: "",
|
||||
showInvalidFeedback: false,
|
||||
isValid: false,
|
||||
timeout: undefined,
|
||||
changed: false,
|
||||
updating: false,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.value = this.store.Configuration.Account[this.targetData];
|
||||
},
|
||||
methods:{
|
||||
async useValidation(){
|
||||
if (this.changed){
|
||||
this.updating = true
|
||||
await fetchPost("/api/updateDashboardConfigurationItem", {
|
||||
section: "Account",
|
||||
key: this.targetData,
|
||||
value: this.value
|
||||
}, (res) => {
|
||||
if (res.status){
|
||||
this.isValid = true;
|
||||
this.showInvalidFeedback = false;
|
||||
this.store.Configuration.Account[this.targetData] = this.value
|
||||
clearTimeout(this.timeout)
|
||||
this.timeout = setTimeout(() => this.isValid = false, 5000);
|
||||
}else{
|
||||
this.isValid = false;
|
||||
this.showInvalidFeedback = true;
|
||||
this.invalidFeedback = res.message
|
||||
}
|
||||
this.changed = false
|
||||
this.updating = false;
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="form-group mb-2">
|
||||
<label :for="this.uuid" class="text-muted mb-1">
|
||||
<strong><small>{{this.title}}</small></strong>
|
||||
</label>
|
||||
<input type="text" class="form-control"
|
||||
:class="{'is-invalid': showInvalidFeedback, 'is-valid': isValid}"
|
||||
:id="this.uuid"
|
||||
v-model="this.value"
|
||||
@keydown="this.changed = true"
|
||||
@blur="useValidation()"
|
||||
:disabled="this.updating"
|
||||
>
|
||||
<div class="invalid-feedback">{{this.invalidFeedback}}</div>
|
||||
<div class="px-2 py-1 text-warning-emphasis bg-warning-subtle border border-warning-subtle rounded-2 d-inline-block mt-1"
|
||||
v-if="warning"
|
||||
>
|
||||
<small><i class="bi bi-exclamation-triangle-fill me-2"></i><span v-html="warningText"></span></small>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@ -0,0 +1,91 @@
|
||||
<script>
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
import {v4} from "uuid";
|
||||
import {fetchPost} from "@/utilities/fetch.js";
|
||||
|
||||
export default {
|
||||
name: "dashboardSettingsInputIPAddressAndPort",
|
||||
props:{
|
||||
// targetData: String,
|
||||
// title: String,
|
||||
// warning: false,
|
||||
// warningText: ""
|
||||
},
|
||||
setup(){
|
||||
const store = DashboardConfigurationStore();
|
||||
const uuid = `input_${v4()}`;
|
||||
return {store, uuid};
|
||||
},
|
||||
data(){
|
||||
return{
|
||||
app_ip:"",
|
||||
app_port:"",
|
||||
invalidFeedback: "",
|
||||
showInvalidFeedback: false,
|
||||
isValid: false,
|
||||
timeout: undefined,
|
||||
changed: false,
|
||||
updating: false,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.app_ip = this.store.Configuration.Server.app_ip;
|
||||
this.app_port = this.store.Configuration.Server.app_port;
|
||||
},
|
||||
methods:{
|
||||
async useValidation(){
|
||||
if(this.changed){
|
||||
await fetchPost("/api/updateDashboardConfigurationItem", {
|
||||
section: "Server",
|
||||
key: this.targetData,
|
||||
value: this.value
|
||||
}, (res) => {
|
||||
if (res.status){
|
||||
this.isValid = true;
|
||||
this.showInvalidFeedback = false;
|
||||
this.store.Configuration.Account[this.targetData] = this.value
|
||||
clearTimeout(this.timeout)
|
||||
this.timeout = setTimeout(() => this.isValid = false, 5000);
|
||||
}else{
|
||||
this.isValid = false;
|
||||
this.showInvalidFeedback = true;
|
||||
this.invalidFeedback = res.message
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="invalid-feedback d-block mt-0">{{this.invalidFeedback}}</div>
|
||||
<div class="row">
|
||||
<div class="form-group mb-2 col-sm">
|
||||
<label :for="'app_ip_' + this.uuid" class="text-muted mb-1">
|
||||
<strong><small>Dashboard IP Address</small></strong>
|
||||
</label>
|
||||
<input type="text" class="form-control mb-2" :id="'app_ip_' + this.uuid" v-model="this.app_ip">
|
||||
<div class="px-2 py-1 text-warning-emphasis bg-warning-subtle border border-warning-subtle rounded-2 d-inline-block">
|
||||
<small><i class="bi bi-exclamation-triangle-fill me-2"></i><code>0.0.0.0</code> means it can be access by anyone with your server
|
||||
IP Address.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-sm">
|
||||
<label :for="'app_port_' + this.uuid" class="text-muted mb-1">
|
||||
<strong><small>Dashboard Port</small></strong>
|
||||
</label>
|
||||
<input type="text" class="form-control mb-2" :id="'app_port_' + this.uuid" v-model="this.app_port">
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-success btn-sm fw-bold rounded-3">
|
||||
<i class="bi bi-floppy-fill me-2"></i>Update Dashboard Settings & Restart
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@ -0,0 +1,86 @@
|
||||
<script>
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
import {v4} from "uuid";
|
||||
import {fetchPost} from "@/utilities/fetch.js";
|
||||
|
||||
export default {
|
||||
name: "dashboardSettingsInputWireguardConfigurationPath",
|
||||
props:{
|
||||
targetData: String,
|
||||
title: String,
|
||||
warning: false,
|
||||
warningText: ""
|
||||
},
|
||||
setup(){
|
||||
const store = DashboardConfigurationStore();
|
||||
const uuid = `input_${v4()}`;
|
||||
return {store, uuid};
|
||||
},
|
||||
data(){
|
||||
return{
|
||||
value:"",
|
||||
invalidFeedback: "",
|
||||
showInvalidFeedback: false,
|
||||
isValid: false,
|
||||
timeout: undefined,
|
||||
changed: false,
|
||||
updating: false,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.value = this.store.Configuration.Server[this.targetData];
|
||||
},
|
||||
methods:{
|
||||
async useValidation(){
|
||||
if(this.changed){
|
||||
await fetchPost("/api/updateDashboardConfigurationItem", {
|
||||
section: "Server",
|
||||
key: this.targetData,
|
||||
value: this.value
|
||||
}, (res) => {
|
||||
if (res.status){
|
||||
this.isValid = true;
|
||||
this.showInvalidFeedback = false;
|
||||
this.store.Configuration.Account[this.targetData] = this.value
|
||||
clearTimeout(this.timeout)
|
||||
this.timeout = setTimeout(() => this.isValid = false, 5000);
|
||||
}else{
|
||||
this.isValid = false;
|
||||
this.showInvalidFeedback = true;
|
||||
this.invalidFeedback = res.message
|
||||
}
|
||||
this.changed = false;
|
||||
this.updating = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="form-group mb-2">
|
||||
<label :for="this.uuid" class="text-muted mb-1">
|
||||
<strong><small>{{this.title}}</small></strong>
|
||||
</label>
|
||||
<input type="text" class="form-control"
|
||||
:class="{'is-invalid': this.showInvalidFeedback, 'is-valid': this.isValid}"
|
||||
:id="this.uuid"
|
||||
v-model="this.value"
|
||||
@keydown="this.changed = true"
|
||||
@blur="this.useValidation()"
|
||||
:disabled="this.updating"
|
||||
>
|
||||
<div class="invalid-feedback">{{this.invalidFeedback}}</div>
|
||||
<div class="px-2 py-1 text-warning-emphasis bg-warning-subtle border border-warning-subtle rounded-2 d-inline-block mt-1"
|
||||
v-if="warning"
|
||||
>
|
||||
<small><i class="bi bi-exclamation-triangle-fill me-2"></i><span v-html="warningText"></span></small>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@ -0,0 +1,49 @@
|
||||
<script>
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
import {fetchPost} from "@/utilities/fetch.js";
|
||||
|
||||
export default {
|
||||
name: "dashboardTheme",
|
||||
setup(){
|
||||
const dashboardConfigurationStore = DashboardConfigurationStore();
|
||||
return {dashboardConfigurationStore}
|
||||
},
|
||||
methods: {
|
||||
async switchTheme(value){
|
||||
await fetchPost("/api/updateDashboardConfigurationItem", {
|
||||
section: "Server",
|
||||
key: "dashboard_theme",
|
||||
value: value
|
||||
}, (res) => {
|
||||
if (res.status){
|
||||
this.dashboardConfigurationStore.Configuration.Server.dashboard_theme = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card mb-4 shadow rounded-3">
|
||||
<p class="card-header">Dashboard Theme</p>
|
||||
<div class="card-body d-flex gap-2">
|
||||
<button class="btn btn-outline-primary flex-grow-1"
|
||||
@click="this.switchTheme('light')"
|
||||
:class="{active: this.dashboardConfigurationStore.Configuration.Server.dashboard_theme === 'light'}">
|
||||
<i class="bi bi-sun-fill"></i>
|
||||
Light
|
||||
</button>
|
||||
<button class="btn btn-outline-primary flex-grow-1"
|
||||
@click="this.switchTheme('dark')"
|
||||
:class="{active: this.dashboardConfigurationStore.Configuration.Server.dashboard_theme === 'dark'}">
|
||||
<i class="bi bi-moon-fill"></i>
|
||||
Dark
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@ -0,0 +1,86 @@
|
||||
<script>
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
import {v4} from "uuid";
|
||||
import {fetchPost} from "@/utilities/fetch.js";
|
||||
|
||||
export default {
|
||||
props:{
|
||||
targetData: String,
|
||||
title: String,
|
||||
warning: false,
|
||||
warningText: "",
|
||||
|
||||
},
|
||||
setup(){
|
||||
const store = DashboardConfigurationStore();
|
||||
const uuid = `input_${v4()}`;
|
||||
return {store, uuid};
|
||||
},
|
||||
data(){
|
||||
return{
|
||||
value:"",
|
||||
invalidFeedback: "",
|
||||
showInvalidFeedback: false,
|
||||
isValid: false,
|
||||
timeout: undefined,
|
||||
changed: false,
|
||||
updating: false,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.value = this.store.Configuration.Peers[this.targetData];
|
||||
},
|
||||
methods:{
|
||||
async useValidation(){
|
||||
if(this.changed){
|
||||
await fetchPost("/api/updateDashboardConfigurationItem", {
|
||||
section: "Peers",
|
||||
key: this.targetData,
|
||||
value: this.value
|
||||
}, (res) => {
|
||||
if (res.status){
|
||||
this.isValid = true;
|
||||
this.showInvalidFeedback = false;
|
||||
this.store.Configuration.Peers[this.targetData] = this.value
|
||||
clearTimeout(this.timeout)
|
||||
this.timeout = setTimeout(() => this.isValid = false, 5000);
|
||||
|
||||
}else{
|
||||
this.isValid = false;
|
||||
this.showInvalidFeedback = true;
|
||||
this.invalidFeedback = res.message
|
||||
}
|
||||
this.changed = false
|
||||
this.updating = false;
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="form-group mb-2">
|
||||
<label :for="this.uuid" class="text-muted mb-1">
|
||||
<strong><small>{{this.title}}</small></strong>
|
||||
</label>
|
||||
<input type="text" class="form-control"
|
||||
:class="{'is-invalid': showInvalidFeedback, 'is-valid': isValid}"
|
||||
:id="this.uuid"
|
||||
v-model="this.value"
|
||||
@keydown="this.changed = true"
|
||||
@blur="useValidation()"
|
||||
:disabled="this.updating"
|
||||
>
|
||||
<div class="invalid-feedback">{{this.invalidFeedback}}</div>
|
||||
<div class="px-2 py-1 text-warning-emphasis bg-warning-subtle border border-warning-subtle rounded-2 d-inline-block mt-1"
|
||||
v-if="warning"
|
||||
>
|
||||
<small><i class="bi bi-exclamation-triangle-fill me-2"></i><span v-html="warningText"></span></small>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
87
src/static/app/src/components/setupComponent/totp.vue
Normal file
87
src/static/app/src/components/setupComponent/totp.vue
Normal file
@ -0,0 +1,87 @@
|
||||
<script>
|
||||
import {fetchGet, fetchPost} from "@/utilities/fetch.js";
|
||||
import QRCode from "qrcode";
|
||||
|
||||
export default {
|
||||
name: "totp",
|
||||
async setup(){
|
||||
let l = ""
|
||||
await fetchGet("/api/Welcome_GetTotpLink", {}, (res => {
|
||||
if (res.status) l = res.data;
|
||||
}));
|
||||
return {l}
|
||||
},
|
||||
mounted() {
|
||||
if (this.l) {
|
||||
QRCode.toCanvas(document.getElementById('qrcode'), this.l, function (error) {})
|
||||
}
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
totp: "",
|
||||
totpInvalidMessage: "",
|
||||
verified: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
validateTotp(){
|
||||
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
totp(newVal){
|
||||
const input = document.querySelector("#totp");
|
||||
input.classList.remove("is-invalid", "is-valid")
|
||||
if (newVal.length === 6){
|
||||
console.log(newVal)
|
||||
if (/[0-9]{6}/.test(newVal)){
|
||||
fetchPost("/api/Welcome_VerifyTotpLink", {
|
||||
totp: newVal
|
||||
}, (res) => {
|
||||
if (res.status){
|
||||
this.verified = true;
|
||||
input.classList.add("is-valid");
|
||||
this.$emit("verified")
|
||||
}else{
|
||||
input.classList.add("is-invalid")
|
||||
this.totpInvalidMessage = "TOTP does not match."
|
||||
}
|
||||
})
|
||||
}else{
|
||||
input.classList.add("is-invalid");
|
||||
this.totpInvalidMessage = "TOTP can only contain numbers"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<p class="mb-2"><small class="text-muted">1. Please scan the following QR Code to generate TOTP</small></p>
|
||||
<canvas id="qrcode" class="rounded-3 mb-2"></canvas>
|
||||
<div class="p-3 bg-body-secondary rounded-3 border mb-3">
|
||||
<p class="text-muted mb-0"><small>Or you can click the link below:</small>
|
||||
</p><a :href="this.l"><code style="line-break: anywhere">{{this.l}}</code></a>
|
||||
</div>
|
||||
<label for="totp" class="mb-2"><small class="text-muted">2. Enter the TOTP generated by your authenticator to verify</small></label>
|
||||
<div class="form-group">
|
||||
<input class="form-control text-center totp"
|
||||
id="totp" maxlength="6" type="text" inputmode="numeric" autocomplete="one-time-code"
|
||||
v-model="this.totp"
|
||||
:disabled="this.verified"
|
||||
>
|
||||
<div class="invalid-feedback">
|
||||
{{this.totpInvalidMessage}}
|
||||
</div>
|
||||
<div class="valid-feedback">
|
||||
TOTP verified!
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@ -3,15 +3,22 @@ import 'bootstrap/dist/css/bootstrap.css'
|
||||
import 'bootstrap/dist/js/bootstrap.js'
|
||||
import 'bootstrap-icons/font/bootstrap-icons.css'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import {createApp, markRaw} from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(createPinia())
|
||||
const app = createApp(App)
|
||||
app.use(router)
|
||||
const pinia = createPinia();
|
||||
|
||||
pinia.use(({ store }) => {
|
||||
store.$router = markRaw(router)
|
||||
})
|
||||
|
||||
app.use(pinia)
|
||||
|
||||
|
||||
app.mount('#app')
|
||||
|
18
src/static/app/src/models/WireguardConfigurations.js
Normal file
18
src/static/app/src/models/WireguardConfigurations.js
Normal file
@ -0,0 +1,18 @@
|
||||
import {fetchGet} from "@/utilities/fetch.js";
|
||||
|
||||
export class WireguardConfigurations{
|
||||
Configurations = undefined;
|
||||
constructor() {
|
||||
this.Configurations = undefined
|
||||
}
|
||||
async initialization(){
|
||||
await this.getConfigurations()
|
||||
}
|
||||
|
||||
|
||||
async getConfigurations(){
|
||||
await fetchGet("/api/getWireguardConfigurations", {}, (res) => {
|
||||
if (res.status) this.Configurations = res.data
|
||||
});
|
||||
}
|
||||
}
|
@ -6,6 +6,10 @@ import ConfigurationList from "@/components/configurationList.vue";
|
||||
import {fetchGet} from "@/utilities/fetch.js";
|
||||
import {wgdashboardStore} from "@/stores/wgdashboardStore.js";
|
||||
import Settings from "@/views/settings.vue";
|
||||
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
import Setup from "@/views/setup.vue";
|
||||
import NewConfiguration from "@/views/newConfiguration.vue";
|
||||
|
||||
const checkAuth = async () => {
|
||||
let result = false
|
||||
@ -35,27 +39,36 @@ const router = createRouter({
|
||||
name: "Settings",
|
||||
path: '/settings',
|
||||
component: Settings
|
||||
},
|
||||
{
|
||||
name: "New Configuration",
|
||||
path: '/new_configuration',
|
||||
component: NewConfiguration
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/signin', component: Signin
|
||||
},
|
||||
{
|
||||
path: '/welcome', component: Setup,
|
||||
meta: {
|
||||
requiresAuth: true
|
||||
},
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
const store = wgdashboardStore();
|
||||
const wireguardConfigurationsStore = WireguardConfigurationsStore();
|
||||
const dashboardConfigurationStore = DashboardConfigurationStore();
|
||||
|
||||
|
||||
if (to.meta.requiresAuth){
|
||||
if (cookie.getCookie("authToken") && await checkAuth()){
|
||||
|
||||
console.log(to.name)
|
||||
if (!store.DashboardConfiguration){
|
||||
await store.getDashboardConfiguration()
|
||||
}
|
||||
if (!store.WireguardConfigurations && to.name !== "Configuration List"){
|
||||
await store.getWireguardConfigurations()
|
||||
await dashboardConfigurationStore.getConfiguration()
|
||||
if (!wireguardConfigurationsStore.Configurations && to.name !== "Configuration List"){
|
||||
await wireguardConfigurationsStore.getConfigurations();
|
||||
}
|
||||
next()
|
||||
}else{
|
||||
|
28
src/static/app/src/stores/DashboardConfigurationStore.js
Normal file
28
src/static/app/src/stores/DashboardConfigurationStore.js
Normal file
@ -0,0 +1,28 @@
|
||||
import {defineStore} from "pinia";
|
||||
import {fetchGet, fetchPost} from "@/utilities/fetch.js";
|
||||
import {cookie} from "@/utilities/cookie.js";
|
||||
|
||||
export const DashboardConfigurationStore = defineStore('DashboardConfigurationStore', {
|
||||
state: () => ({
|
||||
Configuration: undefined
|
||||
}),
|
||||
actions: {
|
||||
async getConfiguration(){
|
||||
await fetchGet("/api/getDashboardConfiguration", {}, (res) => {
|
||||
if (res.status) this.Configuration = res.data
|
||||
});
|
||||
},
|
||||
async updateConfiguration(){
|
||||
await fetchPost("/api/updateDashboardConfiguration", {
|
||||
DashboardConfiguration: this.Configuration
|
||||
}, (res) => {
|
||||
console.log(res)
|
||||
})
|
||||
},
|
||||
async signOut(){
|
||||
await fetchGet("/api/signout", {}, (res) => {
|
||||
this.$router.go('/signin')
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
15
src/static/app/src/stores/WireguardConfigurationsStore.js
Normal file
15
src/static/app/src/stores/WireguardConfigurationsStore.js
Normal file
@ -0,0 +1,15 @@
|
||||
import {defineStore} from "pinia";
|
||||
import {fetchGet} from "@/utilities/fetch.js";
|
||||
|
||||
export const WireguardConfigurationsStore = defineStore('WireguardConfigurationsStore', {
|
||||
state: () => ({
|
||||
Configurations: undefined
|
||||
}),
|
||||
actions: {
|
||||
async getConfigurations(){
|
||||
await fetchGet("/api/getWireguardConfigurations", {}, (res) => {
|
||||
if (res.status) this.Configurations = res.data
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
@ -1,12 +0,0 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useCounterStore = defineStore('counter', () => {
|
||||
const count = ref(0)
|
||||
const doubleCount = computed(() => count.value * 2)
|
||||
function increment() {
|
||||
count.value++
|
||||
}
|
||||
|
||||
return { count, doubleCount, increment }
|
||||
})
|
@ -1,23 +1,20 @@
|
||||
import {defineStore} from "pinia";
|
||||
import {fetchGet} from "@/utilities/fetch.js";
|
||||
|
||||
|
||||
export const wgdashboardStore = defineStore('WGDashboardStore', {
|
||||
state: () => ({
|
||||
state: () => ({
|
||||
WireguardConfigurations: undefined,
|
||||
DashboardConfiguration: undefined
|
||||
}),
|
||||
actions: {
|
||||
async getWireguardConfigurations(){
|
||||
await fetchGet("/api/getWireguardConfigurations", {}, (res) => {
|
||||
console.log(res.status)
|
||||
if (res.status) this.WireguardConfigurations = res.data
|
||||
})
|
||||
},
|
||||
async getDashboardConfiguration(){
|
||||
await fetchGet("/api/getDashboardConfiguration", {}, (res) => {
|
||||
console.log(res.status)
|
||||
if (res.status) this.DashboardConfiguration = res.data
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
3
src/static/app/src/utilities/ipCheck.js
Normal file
3
src/static/app/src/utilities/ipCheck.js
Normal file
@ -0,0 +1,3 @@
|
||||
export const ipV46RegexCheck = (input) => {
|
||||
|
||||
}
|
@ -1,19 +1,30 @@
|
||||
<script>
|
||||
import Navbar from "@/components/navbar.vue";
|
||||
import {wgdashboardStore} from "@/stores/wgdashboardStore.js";
|
||||
import {WireguardConfigurations} from "@/models/WireguardConfigurations.js";
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
|
||||
export default {
|
||||
name: "index",
|
||||
components: {Navbar}
|
||||
components: {Navbar},
|
||||
async setup(){
|
||||
const dashboardConfigurationStore = DashboardConfigurationStore()
|
||||
return {dashboardConfigurationStore}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container-fluid flex-grow-1 main" data-bs-theme="dark">
|
||||
<div class="container-fluid flex-grow-1 main" :data-bs-theme="this.dashboardConfigurationStore.Configuration.Server.dashboard_theme">
|
||||
<div class="row h-100">
|
||||
<Navbar></Navbar>
|
||||
<main class="col-md-9 ml-sm-auto col-lg-10 px-md-4 mb-4">
|
||||
<main class="col-md-9 ml-sm-auto col-lg-10 px-md-4 overflow-y-scroll mb-0" style="height: calc(100vh - 50px)">
|
||||
<Suspense>
|
||||
<RouterView></RouterView>
|
||||
<RouterView v-slot="{Component}">
|
||||
<Transition name="fade2" mode="out-in">
|
||||
<Component :is="Component"></Component>
|
||||
</Transition>
|
||||
</RouterView>
|
||||
</Suspense>
|
||||
</main>
|
||||
</div>
|
||||
|
26
src/static/app/src/views/newConfiguration.vue
Normal file
26
src/static/app/src/views/newConfiguration.vue
Normal file
@ -0,0 +1,26 @@
|
||||
<script>
|
||||
export default {
|
||||
name: "newConfiguration"
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mt-4">
|
||||
<div class="container">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="mb-3 d-flex align-items-center gap-4">
|
||||
<RouterLink to="/">
|
||||
<h3 class="mb-0 text-body">
|
||||
<i class="bi bi-chevron-left"></i>
|
||||
</h3>
|
||||
</RouterLink>
|
||||
<h3 class="text-body mb-0">New Configuration</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@ -1,17 +1,83 @@
|
||||
<script>
|
||||
import {wgdashboardStore} from "@/stores/wgdashboardStore.js";
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
import PeersDefaultSettingsInput from "@/components/settingsComponent/peersDefaultSettingsInput.vue";
|
||||
import {ipV46RegexCheck} from "@/utilities/ipCheck.js";
|
||||
import AccountSettingsInputUsername from "@/components/settingsComponent/accountSettingsInputUsername.vue";
|
||||
import AccountSettingsInputPassword from "@/components/settingsComponent/accountSettingsInputPassword.vue";
|
||||
import DashboardSettingsInputWireguardConfigurationPath
|
||||
from "@/components/settingsComponent/dashboardSettingsInputWireguardConfigurationPath.vue";
|
||||
import DashboardTheme from "@/components/settingsComponent/dashboardTheme.vue";
|
||||
import DashboardSettingsInputIPAddressAndPort
|
||||
from "@/components/settingsComponent/dashboardSettingsInputIPAddressAndPort.vue";
|
||||
|
||||
export default {
|
||||
name: "settings",
|
||||
methods: {ipV46RegexCheck},
|
||||
components: {
|
||||
DashboardSettingsInputIPAddressAndPort,
|
||||
DashboardTheme,
|
||||
DashboardSettingsInputWireguardConfigurationPath,
|
||||
AccountSettingsInputPassword, AccountSettingsInputUsername, PeersDefaultSettingsInput},
|
||||
setup(){
|
||||
const store = wgdashboardStore();
|
||||
return {store}
|
||||
const dashboardConfigurationStore = DashboardConfigurationStore()
|
||||
return {dashboardConfigurationStore}
|
||||
},
|
||||
watch: {
|
||||
// 'dashboardConfigurationStore.Configuration': {
|
||||
// deep: true,
|
||||
// handler(){
|
||||
// this.dashboardConfigurationStore.updateConfiguration();
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mt-4">
|
||||
<div class="container">
|
||||
<h3 class="mb-3 text-body">Settings</h3>
|
||||
<DashboardTheme></DashboardTheme>
|
||||
<div class="card mb-4 shadow rounded-3">
|
||||
<p class="card-header">Peers Default Settings</p>
|
||||
<div class="card-body">
|
||||
<PeersDefaultSettingsInput targetData="peer_global_dns" title="DNS"></PeersDefaultSettingsInput>
|
||||
<PeersDefaultSettingsInput targetData="peer_endpoint_allowed_ip" title="Peer Endpoint Allowed IPs"></PeersDefaultSettingsInput>
|
||||
<PeersDefaultSettingsInput targetData="peer_mtu" title="MTU (Max Transmission Unit)"></PeersDefaultSettingsInput>
|
||||
<PeersDefaultSettingsInput targetData="peer_keep_alive" title="Persistent Keepalive"></PeersDefaultSettingsInput>
|
||||
<PeersDefaultSettingsInput targetData="remote_endpoint" title="Peer Remote Endpoint"
|
||||
:warning="true" warningText="This will be change globally, and will be apply to all peer's QR code and configuration file."
|
||||
></PeersDefaultSettingsInput>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mb-4 shadow rounded-3">
|
||||
<p class="card-header">WireGuard Configurations Settings</p>
|
||||
<div class="card-body">
|
||||
<DashboardSettingsInputWireguardConfigurationPath
|
||||
targetData="wg_conf_path"
|
||||
title="Configurations Directory"
|
||||
:warning="true"
|
||||
warning-text="Remember to remove <code>/</code> at the end of your path. e.g <code>/etc/wireguard</code>"
|
||||
>
|
||||
|
||||
</DashboardSettingsInputWireguardConfigurationPath>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mb-4 shadow rounded-3">
|
||||
<p class="card-header">Account Settings</p>
|
||||
<div class="card-body">
|
||||
<AccountSettingsInputUsername targetData="username"
|
||||
title="Username"
|
||||
></AccountSettingsInputUsername>
|
||||
<hr>
|
||||
<AccountSettingsInputPassword
|
||||
targetData="password">
|
||||
</AccountSettingsInputPassword>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
130
src/static/app/src/views/setup.vue
Normal file
130
src/static/app/src/views/setup.vue
Normal file
@ -0,0 +1,130 @@
|
||||
<script>
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
import QRCode from 'qrcode'
|
||||
import Totp from "@/components/setupComponent/totp.vue";
|
||||
import {fetchPost} from "@/utilities/fetch.js";
|
||||
export default {
|
||||
name: "setup",
|
||||
components: {Totp},
|
||||
setup(){
|
||||
const store = DashboardConfigurationStore();
|
||||
return {store}
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
setup: {
|
||||
username: "",
|
||||
newPassword: "",
|
||||
repeatNewPassword: "",
|
||||
enable_totp: false,
|
||||
verified_totp: false
|
||||
},
|
||||
loading: false,
|
||||
errorMessage: "",
|
||||
done: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
goodToSubmit(){
|
||||
return this.setup.username
|
||||
&& this.setup.newPassword.length >= 8
|
||||
&& this.setup.repeatNewPassword.length >= 8
|
||||
&& this.setup.newPassword === this.setup.repeatNewPassword
|
||||
&& ((this.setup.enable_totp && this.setup.verified_totp) || !this.setup.enable_totp)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submit(){
|
||||
this.loading = true
|
||||
fetchPost("/api/Welcome_Finish", this.setup, (res) => {
|
||||
if (res.status){
|
||||
this.done = true;
|
||||
setTimeout(() => {
|
||||
this.$router.push('/')
|
||||
}, 500)
|
||||
}else{
|
||||
document.querySelectorAll("#createAccount input").forEach(x => x.classList.add("is-invalid"))
|
||||
this.errorMessage = res.message;
|
||||
document.querySelector(".login-container-fluid")
|
||||
.scrollTo({
|
||||
top: 0,
|
||||
left: 0,
|
||||
behavior: "smooth",
|
||||
})
|
||||
}
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container-fluid login-container-fluid d-flex main pt-5 overflow-scroll"
|
||||
:data-bs-theme="this.store.Configuration.Server.dashboard_theme">
|
||||
<div class="mx-auto text-body" style="width: 500px">
|
||||
<span class="dashboardLogo display-4">Nice to meet you!</span>
|
||||
<p class="mb-5">Please fill in the following fields to finish setup 😊</p>
|
||||
<div>
|
||||
<h3>Create an account</h3>
|
||||
<div class="alert alert-danger" v-if="this.errorMessage">
|
||||
{{this.errorMessage}}
|
||||
</div>
|
||||
<div class="d-flex flex-column gap-3">
|
||||
<div id="createAccount">
|
||||
<div class="form-group text-body">
|
||||
<label for="username" class="mb-1 text-muted">
|
||||
<small>Pick an username you like</small></label>
|
||||
<input type="text"
|
||||
v-model="this.setup.username"
|
||||
class="form-control" id="username" name="username" placeholder="Maybe something like 'wiredragon'?" required>
|
||||
</div>
|
||||
<div class="form-group text-body">
|
||||
<label for="password" class="mb-1 text-muted">
|
||||
<small>Create a password (at least 8 characters)</small></label>
|
||||
<input type="password"
|
||||
v-model="this.setup.newPassword"
|
||||
class="form-control" id="password" name="password" placeholder="Make sure is strong enough" required>
|
||||
</div>
|
||||
<div class="form-group text-body">
|
||||
<label for="confirmPassword" class="mb-1 text-muted">
|
||||
<small>Confirm password</small></label>
|
||||
<input type="password"
|
||||
v-model="this.setup.repeatNewPassword"
|
||||
class="form-control" id="confirmPassword" name="confirmPassword" placeholder="and you can remember it :)" required>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="enable_totp"
|
||||
v-model="this.setup.enable_totp">
|
||||
<label class="form-check-label"
|
||||
for="enable_totp">Enable 2 Factor Authentication? <strong>Strongly recommended</strong></label>
|
||||
</div>
|
||||
<Suspense>
|
||||
<Transition name="fade">
|
||||
<Totp v-if="this.setup.enable_totp" @verified="this.setup.verified_totp = true"></Totp>
|
||||
</Transition>
|
||||
</Suspense>
|
||||
|
||||
<button class="btn btn-dark btn-lg mb-5 d-flex btn-brand shadow align-items-center"
|
||||
ref="signInBtn"
|
||||
:disabled="!this.goodToSubmit || this.loading || this.done" @click="this.submit()">
|
||||
<span class="d-flex align-items-center w-100" v-if="!this.loading && !this.done">
|
||||
Finish<i class="bi bi-chevron-right ms-auto"></i></span>
|
||||
<span class="d-flex align-items-center w-100" v-else-if="this.done">
|
||||
Welcome to WGDashboard!</span>
|
||||
<span class="d-flex align-items-center w-100" v-else>
|
||||
Saving...<span class="spinner-border ms-auto spinner-border-sm" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</span></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@ -1,26 +1,48 @@
|
||||
<script>
|
||||
import {fetchPost} from "../utilities/fetch.js";
|
||||
import {fetchGet, fetchPost} from "../utilities/fetch.js";
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
|
||||
export default {
|
||||
name: "signin",
|
||||
async setup(){
|
||||
const store = DashboardConfigurationStore()
|
||||
let theme = ""
|
||||
let totpEnabled = false;
|
||||
await fetchGet("/api/getDashboardTheme", {}, (res) => {
|
||||
theme = res.data
|
||||
});
|
||||
await fetchGet("/api/isTotpEnabled", {}, (res) => {
|
||||
totpEnabled = res.data
|
||||
});
|
||||
return {store, theme, totpEnabled}
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
username: "",
|
||||
password: "",
|
||||
totp: "",
|
||||
loginError: false,
|
||||
loginErrorMessage: ""
|
||||
loginErrorMessage: "",
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async auth(){
|
||||
if (this.username && this.password){
|
||||
if (this.username && this.password && ((this.totpEnabled && this.totp) || !this.totpEnabled)){
|
||||
this.loading = true
|
||||
await fetchPost("/api/authenticate", {
|
||||
username: this.username,
|
||||
password: this.password
|
||||
password: this.password,
|
||||
totp: this.totp
|
||||
}, (response) => {
|
||||
if (response.status){
|
||||
this.loginError = false;
|
||||
this.$router.push('/')
|
||||
this.$refs["signInBtn"].classList.add("signedIn")
|
||||
if (response.message){
|
||||
this.$router.push('/welcome')
|
||||
}else{
|
||||
this.$router.push('/')
|
||||
}
|
||||
}else{
|
||||
this.loginError = true;
|
||||
this.loginErrorMessage = response.message;
|
||||
@ -28,7 +50,9 @@ export default {
|
||||
x.classList.remove("is-valid")
|
||||
x.classList.add("is-invalid")
|
||||
});
|
||||
this.loading = false
|
||||
}
|
||||
|
||||
})
|
||||
}else{
|
||||
document.querySelectorAll("input[required]").forEach(x => {
|
||||
@ -47,25 +71,49 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container-fluid login-container-fluid h-100 d-flex">
|
||||
<div class="container-fluid login-container-fluid d-flex main" :data-bs-theme="this.theme">
|
||||
<div class="login-box m-auto" style="width: 500px;">
|
||||
<h5 class="text-center">Welcome to</h5>
|
||||
<h1 class="text-center">WGDashboard</h1>
|
||||
<h4 class="mb-0 text-body">Welcome to</h4>
|
||||
<span class="dashboardLogo display-3">WGDashboard</span>
|
||||
<div class="m-auto">
|
||||
<div class="alert alert-danger mt-2 mb-0" role="alert" v-if="loginError">
|
||||
{{this.loginErrorMessage}}
|
||||
</div>
|
||||
<form @submit="(e) => {e.preventDefault(); this.auth();}">
|
||||
<div class="form-group">
|
||||
<label for="username" class="text-left" style="font-size: 1rem"><i class="bi bi-person-circle"></i></label>
|
||||
<input type="text" v-model="username" class="form-control" id="username" name="username" placeholder="Username" required>
|
||||
<div class="form-group text-body">
|
||||
<label for="username" class="text-left" style="font-size: 1rem">
|
||||
<i class="bi bi-person-circle"></i></label>
|
||||
<input type="text" v-model="username" class="form-control" id="username" name="username"
|
||||
autocomplete="on"
|
||||
placeholder="Username" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-group text-body">
|
||||
<label for="password" class="text-left" style="font-size: 1rem"><i class="bi bi-key-fill"></i></label>
|
||||
<input type="password" v-model="password" class="form-control" id="password" name="password" placeholder="Password" required>
|
||||
<input type="password"
|
||||
v-model="password" class="form-control" id="password" name="password"
|
||||
autocomplete="on"
|
||||
placeholder="Password" required>
|
||||
</div>
|
||||
<button class="btn btn-dark ms-auto mt-4 w-100 d-flex">
|
||||
Sign In<i class="ms-auto bi bi-chevron-right"></i></button>
|
||||
<div class="form-group text-body" v-if="totpEnabled">
|
||||
<label for="totp" class="text-left" style="font-size: 1rem"><i class="bi bi-lock-fill"></i></label>
|
||||
<input class="form-control totp"
|
||||
required
|
||||
id="totp" maxlength="6" type="text" inputmode="numeric" autocomplete="one-time-code"
|
||||
placeholder="OTP from your authenticator"
|
||||
v-model="this.totp"
|
||||
>
|
||||
</div>
|
||||
<button class="btn btn-lg btn-dark ms-auto mt-4 w-100 d-flex btn-brand shadow signInBtn" ref="signInBtn">
|
||||
<span v-if="!this.loading" class="d-flex w-100">
|
||||
Sign In<i class="ms-auto bi bi-chevron-right"></i>
|
||||
</span>
|
||||
<span v-else class="d-flex w-100 align-items-center">
|
||||
Signing In...
|
||||
<span class="spinner-border ms-auto spinner-border-sm" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -6,6 +6,7 @@ import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
base: "/static/app/dist",
|
||||
plugins: [
|
||||
vue(),
|
||||
],
|
||||
@ -18,5 +19,14 @@ export default defineConfig({
|
||||
proxy: {
|
||||
'/api': proxy
|
||||
}
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
entryFileNames: `assets/[name].js`,
|
||||
chunkFileNames: `assets/[name].js`,
|
||||
assetFileNames: `assets/[name].[ext]`
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -1,10 +1,7 @@
|
||||
#app{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.codeFont{
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
}
|
||||
@ -51,11 +48,57 @@
|
||||
/* position: sticky;*/
|
||||
/* }*/
|
||||
/*}*/
|
||||
[data-bs-theme="dark"].main{
|
||||
|
||||
@property --brandColor1 {
|
||||
syntax: '<color>';
|
||||
initial-value: #009dff;
|
||||
inherits: false;
|
||||
}
|
||||
|
||||
@property --brandColor2 {
|
||||
syntax: '<color>';
|
||||
initial-value: #ff4a00;
|
||||
inherits: false;
|
||||
}
|
||||
|
||||
@property --degree{
|
||||
syntax: '<percentage>';
|
||||
initial-value: 0%;
|
||||
inherits: false;
|
||||
}
|
||||
|
||||
.dashboardLogo{
|
||||
background: rgb(23,139,255);
|
||||
background: linear-gradient(234deg, var(--brandColor1) var(--degree), var(--brandColor2) 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
transition: --brandColor1 1s, --brandColor2 0.3s, --degree 1s !important;
|
||||
|
||||
}
|
||||
|
||||
.btn-brand{
|
||||
/*background: rgb(23,139,255);*/
|
||||
background: linear-gradient(234deg, var(--brandColor1) var(--degree), var(--brandColor2) 100%);
|
||||
border: 0 !important;
|
||||
transition: --brandColor1 1s, --brandColor2 1s, --degree 0.5s !important;
|
||||
}
|
||||
.btn-brand:hover, .dashboardLogo:hover{
|
||||
--brandColor1: #009dff;
|
||||
--brandColor2: #ff875b;
|
||||
--degree: 30%;
|
||||
}
|
||||
|
||||
.signInBtn.signedIn{
|
||||
--degree: 100%;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"].main,
|
||||
#app:has(.main[data-bs-theme="dark"]){
|
||||
background-color: #1b1e21;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.sidebar .nav-link, .bottomNavContainer .nav-link{
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
@ -474,13 +517,18 @@ main {
|
||||
}
|
||||
|
||||
.login-box #username,
|
||||
.login-box #password {
|
||||
.login-box #password,
|
||||
.login-box #totp
|
||||
{
|
||||
padding: 0.6rem calc( 0.9rem + 32px);
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
.login-box label[for="username"],
|
||||
.login-box label[for="password"] {
|
||||
.login-box label[for="password"],
|
||||
.login-box label[for="totp"]
|
||||
|
||||
{
|
||||
font-size: 1rem;
|
||||
margin: 0 !important;
|
||||
transform: translateY(2.1rem) translateX(1rem);
|
||||
@ -488,6 +536,7 @@ main {
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*label[for="password"]{*/
|
||||
|
||||
|
||||
@ -642,10 +691,10 @@ pre.index-alert {
|
||||
margin-left: auto !important;
|
||||
}
|
||||
|
||||
.conf_card .dot,
|
||||
.info .dot {
|
||||
transform: translateX(10px);
|
||||
}
|
||||
/*.conf_card .dot,*/
|
||||
/*.info .dot {*/
|
||||
/* transform: translateX(10px);*/
|
||||
/*}*/
|
||||
|
||||
#config_body {
|
||||
transition: 0.3s ease-in-out;
|
||||
@ -946,4 +995,40 @@ pre.index-alert {
|
||||
|
||||
.theme-switch-btn{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: all 0.3s ease-in-out;
|
||||
/*position: absolute;*/
|
||||
/*padding-top: 50px*/
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
transform: translateY(30px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.fade2-enter-active,
|
||||
.fade2-leave-active {
|
||||
transition: all 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.fade2-enter-from{
|
||||
transform: translateX(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.fade2-leave-to {
|
||||
transform: translateX(-20px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.login-container-fluid{
|
||||
height: calc(100% - 50px) !important;
|
||||
}
|
||||
|
||||
.totp{
|
||||
font-family: var(--bs-font-monospace);
|
||||
}
|
@ -15,22 +15,10 @@
|
||||
<link rel="icon" href="{{ url_for('static',filename='img/logo.png') }}"/>
|
||||
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
|
||||
|
||||
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
||||
<script src="https://unpkg.com/vue-router@4.0.15/dist/vue-router.global.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue-demi@0.14.6/lib/index.iife.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/pinia@2.1.7/dist/pinia.iife.min.js"></script>
|
||||
|
||||
<link rel= "stylesheet" type= "text/css" href= "{{ url_for('static',filename='css/dashboard.css') }}">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/font/bootstrap-icons.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.7.1/chart.min.js" integrity="sha512-QSkVNOCYLtj73J4hbmVoOV6KVZuMluZlioC+trLpewV8qMjsWqlIQvkn1KGX2StWvPMdWGBqim1xlC8krl1EKQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"/>
|
||||
<script src="../static/app/dist/assets/index.js" type="module"></script>
|
||||
<link rel="stylesheet" href="../static/app/dist/assets/index.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" class="vw-100 vh-100"></div>
|
||||
<div id="app" class="w-100 vh-100"></div>
|
||||
</body>
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script><script src="{{ url_for('static',filename='js/tools.js') }}"></script>
|
||||
<script src="../static/app1/app.js" type="module"></script>
|
||||
</html>
|
@ -72,6 +72,7 @@
|
||||
<input type="text" class="form-control mb-4" id="peer_remote_endpoint"
|
||||
name="peer_remote_endpoint"
|
||||
value="{{ peer_remote_endpoint }}" required>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-success" type="submit">Update Peer Default Settings</button>
|
||||
|
Loading…
Reference in New Issue
Block a user