diff --git a/src/dashboard_new.py b/src/dashboard_new.py index 171ea5b..fadf2f5 100644 --- a/src/dashboard_new.py +++ b/src/dashboard_new.py @@ -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) diff --git a/src/static/app/index.html b/src/static/app/index.html index 8e026c5..4f1d70c 100644 --- a/src/static/app/index.html +++ b/src/static/app/index.html @@ -7,7 +7,7 @@ Vite App -
+
diff --git a/src/static/app/package-lock.json b/src/static/app/package-lock.json index 0aeda04..b0c5eb5 100644 --- a/src/static/app/package-lock.json +++ b/src/static/app/package-lock.json @@ -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" + } } } } diff --git a/src/static/app/package.json b/src/static/app/package.json index ca65f0c..681bbe8 100644 --- a/src/static/app/package.json +++ b/src/static/app/package.json @@ -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" }, diff --git a/src/static/app/public/dashboard.css b/src/static/app/public/dashboard.css index 40ff23d..c821f75 100644 --- a/src/static/app/public/dashboard.css +++ b/src/static/app/public/dashboard.css @@ -17,6 +17,10 @@ body { font-weight: bold; } +.dashboardLogo{ + background: -webkit-linear-gradient(#178bff, #ff4a00); +} + /* * Sidebar */ diff --git a/src/static/app/src/App.vue b/src/static/app/src/App.vue index 10bc14e..ffe9f61 100644 --- a/src/static/app/src/App.vue +++ b/src/static/app/src/App.vue @@ -1,6 +1,7 @@ - \ No newline at end of file diff --git a/src/static/app/src/components/configurationList.vue b/src/static/app/src/components/configurationList.vue index be10e2f..243b4a2 100644 --- a/src/static/app/src/components/configurationList.vue +++ b/src/static/app/src/components/configurationList.vue @@ -1,46 +1,48 @@