1
0
mirror of https://github.com/donaldzou/WGDashboard.git synced 2024-11-22 23:27:45 +01:00

Huge update

A welcome session
Added Time based One-Time-Passcode (TOTP)
UI Update
This commit is contained in:
Donald Zou 2024-01-23 15:09:44 -05:00
parent 95a8867527
commit 5f4a364095
32 changed files with 1718 additions and 167 deletions

View File

@ -18,9 +18,11 @@ from json import JSONEncoder
from operator import itemgetter from operator import itemgetter
from typing import Dict, Any from typing import Dict, Any
import bcrypt
import flask import flask
# PIP installed library # PIP installed library
import ifcfg import ifcfg
import pyotp
from flask import Flask, request, render_template, redirect, url_for, session, jsonify, g from flask import Flask, request, render_template, redirect, url_for, session, jsonify, g
from flask.json.provider import JSONProvider from flask.json.provider import JSONProvider
from flask_qrcode import QRcode from flask_qrcode import QRcode
@ -138,15 +140,24 @@ class WireguardConfiguration:
return self.__dict__ 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: class DashboardConfig:
def __init__(self): def __init__(self):
self.__config = configparser.ConfigParser(strict=False) self.__config = configparser.ConfigParser(strict=False)
self.__config.read(DASHBOARD_CONF) self.__config.read(DASHBOARD_CONF)
self.hiddenAttribute = ["totp_key"]
self.__default = { self.__default = {
"Account": { "Account": {
"username": "admin", "username": "admin",
"password": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918" "password": "admin",
"enable_totp": "false",
"totp_key": pyotp.random_base32()
}, },
"Server": { "Server": {
"wg_conf_path": "/etc/wireguard", "wg_conf_path": "/etc/wireguard",
@ -156,7 +167,7 @@ class DashboardConfig:
"version": DASHBOARD_VERSION, "version": DASHBOARD_VERSION,
"dashboard_refresh_interval": "60000", "dashboard_refresh_interval": "60000",
"dashboard_sort": "status", "dashboard_sort": "status",
"dashboard_theme": "light" "dashboard_theme": "dark"
}, },
"Peers": { "Peers": {
"peer_global_DNS": "1.1.1.1", "peer_global_DNS": "1.1.1.1",
@ -165,6 +176,9 @@ class DashboardConfig:
"remote_endpoint": ifcfg.default_interface()['inet'], "remote_endpoint": ifcfg.default_interface()['inet'],
"peer_MTU": "1420", "peer_MTU": "1420",
"peer_keep_alive": "21" "peer_keep_alive": "21"
},
"Other": {
"welcome_session": "true"
} }
} }
@ -172,13 +186,71 @@ class DashboardConfig:
for key, value in keys.items(): for key, value in keys.items():
exist, currentData = self.GetConfig(section, key) exist, currentData = self.GetConfig(section, key)
if not exist: 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: if section not in self.__config:
self.__config[section] = {} self.__config[section] = {}
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 self.__config[section][key] = value
return self.SaveConfig() return self.SaveConfig(), ""
return True, ""
def SaveConfig(self) -> bool: def SaveConfig(self) -> bool:
try: try:
@ -195,13 +267,26 @@ class DashboardConfig:
if key not in self.__config[section]: if key not in self.__config[section]:
return False, None 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] return True, self.__config[section][key]
def toJSON(self) -> dict[str, dict[Any, Any]]: def toJSON(self) -> dict[str, dict[Any, Any]]:
the_dict = {} the_dict = {}
for section in self.__config.sections(): for section in self.__config.sections():
the_dict[section] = {} the_dict[section] = {}
for key, val in self.__config.items(section): for key, val in self.__config.items(section):
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 the_dict[section][key] = val
return the_dict return the_dict
@ -319,10 +404,14 @@ API Routes
@app.before_request @app.before_request
def auth_req(): def auth_req():
authenticationRequired = _strToBool(DashboardConfig.GetConfig("Server", "auth_req")[1]) authenticationRequired = DashboardConfig.GetConfig("Server", "auth_req")[1]
if authenticationRequired: if authenticationRequired:
if ('/static/' not in request.path and "username" not in session and "/" != request.path 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 = Flask.make_response(app, "Not Authorized" + request.path)
resp.status_code = 401 resp.status_code = 401
return resp return resp
@ -340,17 +429,32 @@ def API_ValidateAuthentication():
@app.route('/api/authenticate', methods=['POST']) @app.route('/api/authenticate', methods=['POST'])
def API_AuthenticateLogin(): def API_AuthenticateLogin():
data = request.get_json() data = request.get_json()
password = hashlib.sha256(data['password'].encode()) valid = bcrypt.checkpw(data['password'].encode("utf-8"),
print() DashboardConfig.GetConfig("Account", "password")[1].encode("utf-8"))
if password.hexdigest() == DashboardConfig.GetConfig("Account", "password")[1] \ totpEnabled = DashboardConfig.GetConfig("Account", "enable_totp")[1]
and data['username'] == DashboardConfig.GetConfig("Account", "username")[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() authToken = hashlib.sha256(f"{data['username']}{datetime.now()}".encode()).hexdigest()
session['username'] = authToken session['username'] = authToken
resp = ResponseObject(True, "") resp = ResponseObject(True, DashboardConfig.GetConfig("Other", "welcome_session")[1])
resp.set_cookie("authToken", authToken) resp.set_cookie("authToken", authToken)
session.permanent = True session.permanent = True
return resp 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"]) @app.route('/api/getWireguardConfigurations', methods=["GET"])
@ -364,6 +468,95 @@ def API_getDashboardConfiguration():
return ResponseObject(data=DashboardConfig.toJSON()) 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__": if __name__ == "__main__":
engine = create_engine("sqlite:///" + os.path.join(CONFIGURATION_PATH, 'db', 'wgdashboard.db')) engine = create_engine("sqlite:///" + os.path.join(CONFIGURATION_PATH, 'db', 'wgdashboard.db'))
@ -371,5 +564,4 @@ if __name__ == "__main__":
_, app_port = DashboardConfig.GetConfig("Server", "app_port") _, app_port = DashboardConfig.GetConfig("Server", "app_port")
_, WG_CONF_PATH = DashboardConfig.GetConfig("Server", "wg_conf_path") _, WG_CONF_PATH = DashboardConfig.GetConfig("Server", "wg_conf_path")
WireguardConfigurations = _getConfigurationList() WireguardConfigurations = _getConfigurationList()
app.run(host=app_ip, debug=True, port=app_port)
app.run(host=app_ip, debug=False, port=app_port)

View File

@ -7,7 +7,7 @@
<title>Vite App</title> <title>Vite App</title>
</head> </head>
<body> <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> <script type="module" src="./src/main.js"></script>
</body> </body>
</html> </html>

View File

@ -11,6 +11,8 @@
"bootstrap": "^5.3.2", "bootstrap": "^5.3.2",
"bootstrap-icons": "^1.11.2", "bootstrap-icons": "^1.11.2",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"qrcode": "^1.5.3",
"uuid": "^9.0.1",
"vue": "^3.3.11", "vue": "^3.3.11",
"vue-router": "^4.2.5" "vue-router": "^4.2.5"
}, },
@ -690,6 +692,28 @@
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.3.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.3.tgz",
"integrity": "sha512-rIwlkkP1n4uKrRzivAKPZIEkHiuwY5mmhMJ2nZKCBLz8lTUlE73rQh4n1OnnMurXt1vcUNyH4ZPfdh8QweTjpQ==" "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": { "node_modules/bootstrap": {
"version": "5.3.2", "version": "5.3.2",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.2.tgz", "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": { "node_modules/csstype": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" "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": { "node_modules/entities": {
"version": "4.5.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "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", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" "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": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "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": "^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": { "node_modules/magic-string": {
"version": "0.30.5", "version": "0.30.5",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", "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": "^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": { "node_modules/picocolors": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", "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": { "node_modules/postcss": {
"version": "8.4.32", "version": "8.4.32",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz",
@ -906,6 +1075,36 @@
"node": "^10 || ^12 || >=14" "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": { "node_modules/rollup": {
"version": "4.9.2", "version": "4.9.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.2.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.2.tgz",
@ -935,6 +1134,11 @@
"fsevents": "~2.3.2" "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": { "node_modules/source-map-js": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
@ -943,6 +1147,42 @@
"node": ">=0.10.0" "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": { "node_modules/vite": {
"version": "5.0.10", "version": "5.0.10",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.0.10.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.10.tgz",
@ -1031,6 +1271,62 @@
"peerDependencies": { "peerDependencies": {
"vue": "^3.2.0" "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"
}
} }
} }
} }

View File

@ -12,6 +12,8 @@
"bootstrap": "^5.3.2", "bootstrap": "^5.3.2",
"bootstrap-icons": "^1.11.2", "bootstrap-icons": "^1.11.2",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"qrcode": "^1.5.3",
"uuid": "^9.0.1",
"vue": "^3.3.11", "vue": "^3.3.11",
"vue-router": "^4.2.5" "vue-router": "^4.2.5"
}, },

View File

@ -17,6 +17,10 @@ body {
font-weight: bold; font-weight: bold;
} }
.dashboardLogo{
background: -webkit-linear-gradient(#178bff, #ff4a00);
}
/* /*
* Sidebar * Sidebar
*/ */

View File

@ -1,6 +1,7 @@
<script setup > <script setup >
import { RouterView } from 'vue-router' import { RouterView } from 'vue-router'
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
const store = DashboardConfigurationStore();
</script> </script>
<template> <template>
@ -9,5 +10,11 @@ import { RouterView } from 'vue-router'
<span class="navbar-brand mb-0 h1">WGDashboard</span> <span class="navbar-brand mb-0 h1">WGDashboard</span>
</div> </div>
</nav> </nav>
<RouterView></RouterView> <Suspense>
<RouterView v-slot="{ Component }">
<Transition name="fade" mode="out-in">
<Component :is="Component"></Component>
</Transition>
</RouterView>
</Suspense>
</template> </template>

View File

@ -1,46 +1,48 @@
<script> <script>
import {wgdashboardStore} from "@/stores/wgdashboardStore.js"; import {wgdashboardStore} from "@/stores/wgdashboardStore.js";
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
export default { export default {
name: "configurationList", name: "configurationList",
async setup(){ async setup(){
const store = wgdashboardStore(); const wireguardConfigurationsStore = WireguardConfigurationsStore();
await store.getWireguardConfigurations(); await wireguardConfigurationsStore.getConfigurations();
return {store} return {wireguardConfigurationsStore}
} }
} }
</script> </script>
<template> <template>
<div class="mt-4"> <div class="mt-4">
<h3 class="mb-3 text-body">Wireguard Configurations</h3> <div class="container">
<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="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="card conf_card rounded-3 shadow" v-else v-for="c in this.store.WireguardConfigurations" :key="c.Name"> <div class="d-flex gap-3 flex-column" v-else >
<div class="card-body"> <RouterLink :to="'/configuration/' + c.Name"
<div class="row"> class="card conf_card rounded-3 shadow text-decoration-none" v-for="c in this.wireguardConfigurationsStore.Configurations" :key="c.Name">
<div class="row"> <div class="card-body d-flex align-items-center gap-3 flex-wrap">
<div class="col card-col"> <h6 class="mb-0"><span class="dot" :class="{active: c.Status}"></span></h6>
<small class="text-muted"><strong>CONFIGURATION</strong></small> <h6 class="card-title mb-0"><samp>{{c.Name}}</samp></h6>
<h6 class="card-title" style="margin:0 !important;"><samp>{{c.Name}}</samp></h6> <h6 class="mb-0 ms-auto">
</div> <i class="bi bi-chevron-right"></i>
<div class="col card-col"> </h6>
<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> </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> </div>
</RouterLink>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,17 +1,21 @@
<script> <script>
import {wgdashboardStore} from "@/stores/wgdashboardStore.js"; import {wgdashboardStore} from "@/stores/wgdashboardStore.js";
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
export default { export default {
name: "navbar", name: "navbar",
setup(){ setup(){
const store = wgdashboardStore() const wireguardConfigurationsStore = WireguardConfigurationsStore();
return {store} const dashboardConfigurationStore = DashboardConfigurationStore();
return {wireguardConfigurationsStore, dashboardConfigurationStore}
} }
} }
</script> </script>
<template> <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="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"> <div class="sidebar-sticky pt-3">
<ul class="nav flex-column"> <ul class="nav flex-column">
<li class="nav-item"> <li class="nav-item">
@ -22,11 +26,10 @@ export default {
<hr> <hr>
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted"> <h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
<span>Configurations</span> <span>Configurations</span>
</h6> </h6>
<ul class="nav flex-column"> <ul class="nav flex-column">
<li class="nav-item"> <li class="nav-item">
<RouterLink :to="'/configuration/'+c.Name" class="nav-link nav-conf-link" v-for="c in this.store.WireguardConfigurations"> <RouterLink :to="'/configuration/'+c.Name" class="nav-link nav-conf-link" v-for="c in this.wireguardConfigurationsStore.Configurations">
<samp>{{c.Name}}</samp> <samp>{{c.Name}}</samp>
</RouterLink> </RouterLink>
</li> </li>
@ -40,11 +43,17 @@ export default {
<li class="nav-item"><a class="nav-link" data-toggle="modal" data-target="#traceroute_modal" href="#">Traceroute</a></li> <li class="nav-item"><a class="nav-link" data-toggle="modal" data-target="#traceroute_modal" href="#">Traceroute</a></li>
</ul> </ul>
<hr> <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"> <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> <li class="nav-item"><a href="https://github.com/donaldzou/WGDashboard/releases/tag/"><small class="nav-link text-muted"></small></a></li>
</ul> </ul>
</div> </div>
</nav> </nav>
</div>
</template> </template>
<style scoped> <style scoped>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View 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>

View File

@ -3,15 +3,22 @@ import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap/dist/js/bootstrap.js' import 'bootstrap/dist/js/bootstrap.js'
import 'bootstrap-icons/font/bootstrap-icons.css' import 'bootstrap-icons/font/bootstrap-icons.css'
import { createApp } from 'vue' import {createApp, markRaw} from 'vue'
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
const app = createApp(App)
app.use(createPinia()) const app = createApp(App)
app.use(router) app.use(router)
const pinia = createPinia();
pinia.use(({ store }) => {
store.$router = markRaw(router)
})
app.use(pinia)
app.mount('#app') app.mount('#app')

View 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
});
}
}

View File

@ -6,6 +6,10 @@ import ConfigurationList from "@/components/configurationList.vue";
import {fetchGet} from "@/utilities/fetch.js"; import {fetchGet} from "@/utilities/fetch.js";
import {wgdashboardStore} from "@/stores/wgdashboardStore.js"; import {wgdashboardStore} from "@/stores/wgdashboardStore.js";
import Settings from "@/views/settings.vue"; 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 () => { const checkAuth = async () => {
let result = false let result = false
@ -35,27 +39,36 @@ const router = createRouter({
name: "Settings", name: "Settings",
path: '/settings', path: '/settings',
component: Settings component: Settings
},
{
name: "New Configuration",
path: '/new_configuration',
component: NewConfiguration
} }
] ]
}, },
{ {
path: '/signin', component: Signin path: '/signin', component: Signin
},
{
path: '/welcome', component: Setup,
meta: {
requiresAuth: true
},
} }
] ]
}); });
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, from, next) => {
const store = wgdashboardStore(); const wireguardConfigurationsStore = WireguardConfigurationsStore();
const dashboardConfigurationStore = DashboardConfigurationStore();
if (to.meta.requiresAuth){ if (to.meta.requiresAuth){
if (cookie.getCookie("authToken") && await checkAuth()){ if (cookie.getCookie("authToken") && await checkAuth()){
await dashboardConfigurationStore.getConfiguration()
console.log(to.name) if (!wireguardConfigurationsStore.Configurations && to.name !== "Configuration List"){
if (!store.DashboardConfiguration){ await wireguardConfigurationsStore.getConfigurations();
await store.getDashboardConfiguration()
}
if (!store.WireguardConfigurations && to.name !== "Configuration List"){
await store.getWireguardConfigurations()
} }
next() next()
}else{ }else{

View 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')
});
}
}
});

View 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
});
}
}
});

View File

@ -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 }
})

View File

@ -1,23 +1,20 @@
import {defineStore} from "pinia"; import {defineStore} from "pinia";
import {fetchGet} from "@/utilities/fetch.js"; import {fetchGet} from "@/utilities/fetch.js";
export const wgdashboardStore = defineStore('WGDashboardStore', { export const wgdashboardStore = defineStore('WGDashboardStore', {
state: () => ({ state: () => ({
WireguardConfigurations: undefined, WireguardConfigurations: undefined,
DashboardConfiguration: undefined DashboardConfiguration: undefined
}), }),
actions: { actions: {
async getWireguardConfigurations(){
await fetchGet("/api/getWireguardConfigurations", {}, (res) => {
console.log(res.status)
if (res.status) this.WireguardConfigurations = res.data
})
},
async getDashboardConfiguration(){ async getDashboardConfiguration(){
await fetchGet("/api/getDashboardConfiguration", {}, (res) => { await fetchGet("/api/getDashboardConfiguration", {}, (res) => {
console.log(res.status) console.log(res.status)
if (res.status) this.DashboardConfiguration = res.data if (res.status) this.DashboardConfiguration = res.data
}) })
} }
}, }
}) });

View File

@ -0,0 +1,3 @@
export const ipV46RegexCheck = (input) => {
}

View File

@ -1,19 +1,30 @@
<script> <script>
import Navbar from "@/components/navbar.vue"; 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 { export default {
name: "index", name: "index",
components: {Navbar} components: {Navbar},
async setup(){
const dashboardConfigurationStore = DashboardConfigurationStore()
return {dashboardConfigurationStore}
}
} }
</script> </script>
<template> <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"> <div class="row h-100">
<Navbar></Navbar> <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> <Suspense>
<RouterView></RouterView> <RouterView v-slot="{Component}">
<Transition name="fade2" mode="out-in">
<Component :is="Component"></Component>
</Transition>
</RouterView>
</Suspense> </Suspense>
</main> </main>
</div> </div>

View 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>

View File

@ -1,17 +1,83 @@
<script> <script>
import {wgdashboardStore} from "@/stores/wgdashboardStore.js"; 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 { export default {
name: "settings", name: "settings",
methods: {ipV46RegexCheck},
components: {
DashboardSettingsInputIPAddressAndPort,
DashboardTheme,
DashboardSettingsInputWireguardConfigurationPath,
AccountSettingsInputPassword, AccountSettingsInputUsername, PeersDefaultSettingsInput},
setup(){ setup(){
const store = wgdashboardStore(); const dashboardConfigurationStore = DashboardConfigurationStore()
return {store} return {dashboardConfigurationStore}
},
watch: {
// 'dashboardConfigurationStore.Configuration': {
// deep: true,
// handler(){
// this.dashboardConfigurationStore.updateConfiguration();
// }
// }
} }
} }
</script> </script>
<template> <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> </template>
<style scoped> <style scoped>

View 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>

View File

@ -1,26 +1,48 @@
<script> <script>
import {fetchPost} from "../utilities/fetch.js"; import {fetchGet, fetchPost} from "../utilities/fetch.js";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
export default { export default {
name: "signin", 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(){ data(){
return { return {
username: "", username: "",
password: "", password: "",
totp: "",
loginError: false, loginError: false,
loginErrorMessage: "" loginErrorMessage: "",
loading: false
} }
}, },
methods: { methods: {
async auth(){ 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", { await fetchPost("/api/authenticate", {
username: this.username, username: this.username,
password: this.password password: this.password,
totp: this.totp
}, (response) => { }, (response) => {
if (response.status){ if (response.status){
this.loginError = false; this.loginError = false;
this.$refs["signInBtn"].classList.add("signedIn")
if (response.message){
this.$router.push('/welcome')
}else{
this.$router.push('/') this.$router.push('/')
}
}else{ }else{
this.loginError = true; this.loginError = true;
this.loginErrorMessage = response.message; this.loginErrorMessage = response.message;
@ -28,7 +50,9 @@ export default {
x.classList.remove("is-valid") x.classList.remove("is-valid")
x.classList.add("is-invalid") x.classList.add("is-invalid")
}); });
this.loading = false
} }
}) })
}else{ }else{
document.querySelectorAll("input[required]").forEach(x => { document.querySelectorAll("input[required]").forEach(x => {
@ -47,25 +71,49 @@ export default {
</script> </script>
<template> <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;"> <div class="login-box m-auto" style="width: 500px;">
<h5 class="text-center">Welcome to</h5> <h4 class="mb-0 text-body">Welcome to</h4>
<h1 class="text-center">WGDashboard</h1> <span class="dashboardLogo display-3">WGDashboard</span>
<div class="m-auto"> <div class="m-auto">
<div class="alert alert-danger mt-2 mb-0" role="alert" v-if="loginError"> <div class="alert alert-danger mt-2 mb-0" role="alert" v-if="loginError">
{{this.loginErrorMessage}} {{this.loginErrorMessage}}
</div> </div>
<form @submit="(e) => {e.preventDefault(); this.auth();}"> <form @submit="(e) => {e.preventDefault(); this.auth();}">
<div class="form-group"> <div class="form-group text-body">
<label for="username" class="text-left" style="font-size: 1rem"><i class="bi bi-person-circle"></i></label> <label for="username" class="text-left" style="font-size: 1rem">
<input type="text" v-model="username" class="form-control" id="username" name="username" placeholder="Username" required> <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>
<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> <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> </div>
<button class="btn btn-dark ms-auto mt-4 w-100 d-flex"> <div class="form-group text-body" v-if="totpEnabled">
Sign In<i class="ms-auto bi bi-chevron-right"></i></button> <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> </form>
</div> </div>
</div> </div>

View File

@ -6,6 +6,7 @@ import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
base: "/static/app/dist",
plugins: [ plugins: [
vue(), vue(),
], ],
@ -18,5 +19,14 @@ export default defineConfig({
proxy: { proxy: {
'/api': proxy '/api': proxy
} }
},
build: {
rollupOptions: {
output: {
entryFileNames: `assets/[name].js`,
chunkFileNames: `assets/[name].js`,
assetFileNames: `assets/[name].[ext]`
}
}
} }
}) })

View File

@ -1,10 +1,7 @@
#app{ ::-webkit-scrollbar {
display: flex; display: none;
flex-direction: column;
} }
.codeFont{ .codeFont{
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
} }
@ -51,11 +48,57 @@
/* position: sticky;*/ /* 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; background-color: #1b1e21;
} }
.sidebar .nav-link, .bottomNavContainer .nav-link{ .sidebar .nav-link, .bottomNavContainer .nav-link{
font-weight: 500; font-weight: 500;
color: #333; color: #333;
@ -474,13 +517,18 @@ main {
} }
.login-box #username, .login-box #username,
.login-box #password { .login-box #password,
.login-box #totp
{
padding: 0.6rem calc( 0.9rem + 32px); padding: 0.6rem calc( 0.9rem + 32px);
height: inherit; height: inherit;
} }
.login-box label[for="username"], .login-box label[for="username"],
.login-box label[for="password"] { .login-box label[for="password"],
.login-box label[for="totp"]
{
font-size: 1rem; font-size: 1rem;
margin: 0 !important; margin: 0 !important;
transform: translateY(2.1rem) translateX(1rem); transform: translateY(2.1rem) translateX(1rem);
@ -488,6 +536,7 @@ main {
} }
/*label[for="password"]{*/ /*label[for="password"]{*/
@ -642,10 +691,10 @@ pre.index-alert {
margin-left: auto !important; margin-left: auto !important;
} }
.conf_card .dot, /*.conf_card .dot,*/
.info .dot { /*.info .dot {*/
transform: translateX(10px); /* transform: translateX(10px);*/
} /*}*/
#config_body { #config_body {
transition: 0.3s ease-in-out; transition: 0.3s ease-in-out;
@ -947,3 +996,39 @@ pre.index-alert {
.theme-switch-btn{ .theme-switch-btn{
width: 100%; 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);
}

View File

@ -15,22 +15,10 @@
<link rel="icon" href="{{ url_for('static',filename='img/logo.png') }}"/> <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="../static/app/dist/assets/index.js" type="module"></script>
<link rel="stylesheet" href="../static/app/dist/assets/index.css">
<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"/>
</head> </head>
<body> <body>
<div id="app" class="vw-100 vh-100"></div> <div id="app" class="w-100 vh-100"></div>
</body> </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> </html>

View File

@ -72,6 +72,7 @@
<input type="text" class="form-control mb-4" id="peer_remote_endpoint" <input type="text" class="form-control mb-4" id="peer_remote_endpoint"
name="peer_remote_endpoint" name="peer_remote_endpoint"
value="{{ peer_remote_endpoint }}" required> value="{{ peer_remote_endpoint }}" required>
</div> </div>
</div> </div>
<button class="btn btn-success" type="submit">Update Peer Default Settings</button> <button class="btn btn-success" type="submit">Update Peer Default Settings</button>