diff --git a/src/dashboard.py b/src/dashboard.py index e0036d6..72a47a0 100644 --- a/src/dashboard.py +++ b/src/dashboard.py @@ -288,9 +288,6 @@ def get_transfer(config_name): # {round(cumulative_sent + cumulative_receive, 4)}, '{now_string}') # ''') - - - def get_endpoint(config_name): """ Get endpoint from all peers of a configuration @@ -310,8 +307,6 @@ def get_endpoint(config_name): % (data_usage[count + 1], data_usage[count])) count += 2 - - def get_allowed_ip(conf_peer_data, config_name): """ Get allowed ips from all peers of a configuration @@ -324,8 +319,6 @@ def get_allowed_ip(conf_peer_data, config_name): g.cur.execute("UPDATE " + config_name + " SET allowed_ip = '%s' WHERE id = '%s'" % (i.get('AllowedIPs', '(None)'), i["PublicKey"])) - - def get_all_peers_data(config_name): """ Look for new peers from WireGuard @@ -488,7 +481,6 @@ def get_conf_total_data(config_name): download_total = round(download_total, 4) return [total, upload_total, download_total] - def get_conf_status(config_name): """ Check if the configuration is running or not @@ -690,9 +682,8 @@ def auth_req(): @return: Redirect """ return None - - if getattr(g, 'db', None) is None: + print('hi') g.db = connect_db() g.cur = g.db.cursor() conf = get_dashboard_conf() @@ -781,6 +772,7 @@ def auth(): return jsonify({"status": False, "msg": "Username or Password is incorrect."}) + """ Index Page """ @@ -796,8 +788,8 @@ def index(): if "switch_msg" in session: msg = session["switch_msg"] session.pop("switch_msg") - return render_template('index_new.html') - # return render_template('index.html', conf=get_conf_list(), msg=msg) + # return render_template('index_new.html') + return render_template('index.html', conf=get_conf_list(), msg=msg) # Setting Page @@ -1787,6 +1779,37 @@ def traceroute_ip(): return jsonify(returnjson) except Exception: return "Error" + +### NEW API ROUTES + +@app.route('/api/authenticate', methods=['POST']) +def api_auth(): + """ + Authentication request + @return: json object indicating verifying + """ + data = request.get_json() + config = get_dashboard_conf() + password = hashlib.sha256(data['password'].encode()) + if password.hexdigest() == config["Account"]["password"] \ + and data['username'] == config["Account"]["username"]: + session['username'] = data['username'] + resp = jsonify(ResponseObject(True)) + resp.set_cookie("authToken", hashlib.sha256(f"{data['username']}{datetime.now()}".encode()).hexdigest()) + session.permanent = True + config.clear() + return resp + + config.clear() + return jsonify(ResponseObject(False, "Username or password in incorrect.")) + + +def ResponseObject(status = True, message = None, data = None) -> dict: + return { + "status": status, + "message": message, + "data": data + } import atexit @@ -1911,8 +1934,6 @@ def init_dashboard(): """ Create dashboard default configuration. """ - - # Set Default INI File if not os.path.isfile(DASHBOARD_CONF): open(DASHBOARD_CONF, "w+").close() diff --git a/src/dashboard_new.py b/src/dashboard_new.py new file mode 100644 index 0000000..7a89375 --- /dev/null +++ b/src/dashboard_new.py @@ -0,0 +1,197 @@ +from crypt import methods +import sqlite3 +import configparser +import hashlib +import ipaddress +import json +# Python Built-in Library +import os +import secrets +import subprocess +import time +import re +import urllib.parse +import urllib.request +import urllib.error +from datetime import datetime, timedelta +from operator import itemgetter +# PIP installed library +import ifcfg +from flask import Flask, request, render_template, redirect, url_for, session, jsonify, g +from flask_qrcode import QRcode +from icmplib import ping, traceroute + +# Import other python files +import threading + +from sqlalchemy.orm import mapped_column, declarative_base, Session +from sqlalchemy import FLOAT, INT, VARCHAR, select, MetaData +from sqlalchemy import create_engine + +DASHBOARD_VERSION = 'v3.1' +CONFIGURATION_PATH = os.getenv('CONFIGURATION_PATH', '.') +DB_PATH = os.path.join(CONFIGURATION_PATH, 'db') +if not os.path.isdir(DB_PATH): + os.mkdir(DB_PATH) +DASHBOARD_CONF = os.path.join(CONFIGURATION_PATH, 'wg-dashboard.ini') + +# WireGuard's configuration path +WG_CONF_PATH = None +# Dashboard Config Name +# Upgrade Required +UPDATE = None +# Flask App Configuration +app = Flask("WGDashboard") +app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 5206928 +app.secret_key = secrets.token_urlsafe(32) +# Enable QR Code Generator +QRcode(app) + +''' +Classes +''' +Base = declarative_base() + + +class DashboardConfig: + + def __init__(self): + self.__config = configparser.ConfigParser(strict=False) + self.__config.read(DASHBOARD_CONF) + self.__default = { + "Account": { + "username": "admin", + "password": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918" + }, + "Server": { + "wg_conf_path": "/etc/wireguard", + "app_ip": "0.0.0.0", + "app_port": "10086", + "auth_req": "true", + "version": DASHBOARD_VERSION, + "dashboard_refresh_interval": "60000", + "dashboard_sort": "status", + "dashboard_theme": "light" + }, + "Peers": { + "peer_global_DNS": "1.1.1.1", + "peer_endpoint_allowed_ip": "0.0.0.0/0", + "peer_display_mode": "grid", + "remote_endpoint": ifcfg.default_interface()['inet'], + "peer_MTU": "1420", + "peer_keep_alive": "21" + } + } + + for section, keys in self.__default.items(): + for key, value in keys.items(): + exist, currentData = self.GetConfig(section, key) + if not exist: + self.SetConfig(section, key, value) + + 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() + + def SaveConfig(self) -> bool: + try: + with open(DASHBOARD_CONF, "w+", encoding='utf-8') as configFile: + self.__config.write(configFile) + return True + except Exception as e: + return False + + def GetConfig(self, section, key) -> [any, bool]: + if section not in self.__config: + return False, None + + if key not in self.__config[section]: + return False, None + + return True, self.__config[section][key] + + +def ResponseObject(status=True, message=None, data=None) -> dict: + return { + "status": status, + "message": message, + "data": data + } + + +DashboardConfig = DashboardConfig() + +''' +Private Functions +''' + + +def _createPeerModel(wgConfigName): + class Peer(Base): + __tablename__ = wgConfigName + id = mapped_column(VARCHAR, primary_key=True) + private_key = mapped_column(VARCHAR) + DNS = mapped_column(VARCHAR) + endpoint_allowed_ip = mapped_column(VARCHAR) + name = mapped_column(VARCHAR) + total_receive = mapped_column(FLOAT) + total_sent = mapped_column(FLOAT) + total_data = mapped_column(FLOAT) + endpoint = mapped_column(VARCHAR) + status = mapped_column(VARCHAR) + latest_handshake = mapped_column(VARCHAR) + allowed_ip = mapped_column(VARCHAR) + cumu_receive = mapped_column(FLOAT) + cumu_sent = mapped_column(FLOAT) + cumu_data = mapped_column(FLOAT) + mtu = mapped_column(INT) + keepalive = mapped_column(INT) + remote_endpoint = mapped_column(VARCHAR) + preshared_key = mapped_column(VARCHAR) + + return Peer + + +def _regexMatch(regex, text): + pattern = re.compile(regex) + return pattern.search(text) is not None + + +def _getConfigurationList(): + conf = [] + for i in os.listdir(WG_CONF_PATH): + if _regexMatch("^(.{1,}).(conf)$", i): + i = i.replace('.conf', '') + _createPeerModel(i).__table__.create(engine) + _createPeerModel(i + "_restrict_access").__table__.create(engine) + + +''' +API Routes +''' + + +@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]: + session['username'] = data['username'] + resp = jsonify(ResponseObject(True)) + resp.set_cookie("authToken", + hashlib.sha256(f"{data['username']}{datetime.now()}".encode()).hexdigest()) + session.permanent = True + return resp + return jsonify(ResponseObject(False, "Username or password is incorrect.")) + + +if __name__ == "__main__": + engine = create_engine("sqlite:///" + os.path.join(CONFIGURATION_PATH, 'db', 'wgdashboard.db')) + _, app_ip = DashboardConfig.GetConfig("Server", "app_ip") + _, app_port = DashboardConfig.GetConfig("Server", "app_port") + _getConfigurationList() + app.run(host=app_ip, debug=False, port=app_port) diff --git a/src/static/app/index.html b/src/static/app/index.html index 456879a..8e026c5 100644 --- a/src/static/app/index.html +++ b/src/static/app/index.html @@ -8,6 +8,6 @@
- + diff --git a/src/static/app/public/dashboard.css b/src/static/app/public/dashboard.css new file mode 100644 index 0000000..40ff23d --- /dev/null +++ b/src/static/app/public/dashboard.css @@ -0,0 +1,934 @@ +body { + font-size: .875rem; + /*font-family: 'Poppins', sans-serif;*/ +} + +.codeFont{ + font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; +} + +.feather { + width: 16px; + height: 16px; + vertical-align: text-bottom; +} + +.btn-primary { + font-weight: bold; +} + +/* + * Sidebar + */ + +/*.sidebar {*/ +/* position: fixed;*/ +/* top: 0;*/ +/* bottom: 0;*/ +/* left: 0;*/ +/* z-index: 100;*/ +/* !* Behind the navbar *!*/ +/* padding: 48px 0 0;*/ +/* !* Height of navbar *!*/ +/* box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);*/ +/*}*/ + +/*.sidebar-sticky {*/ +/* position: relative;*/ +/* top: 0;*/ +/* height: calc(100vh - 48px);*/ +/* padding-top: .5rem;*/ +/* overflow-x: hidden;*/ +/* overflow-y: auto;*/ +/* !* Scrollable contents if viewport is shorter than content. *!*/ +/*}*/ + +/*@supports ((position: -webkit-sticky) or (position: sticky)) {*/ +/* .sidebar-sticky {*/ +/* position: -webkit-sticky;*/ +/* position: sticky;*/ +/* }*/ +/*}*/ + +.sidebar .nav-link, .bottomNavContainer .nav-link{ + font-weight: 500; + color: #333; + transition: 0.2s cubic-bezier(0.82, -0.07, 0, 1.01); +} + +.nav-link:hover { + padding-left: 30px; + background-color: #dfdfdf; +} + +.sidebar .nav-link .feather { + margin-right: 4px; + color: #999; +} + +.sidebar .nav-link.active, .bottomNavContainer .nav-link.active { + color: #007bff; +} + +.sidebar .nav-link:hover .feather, +.sidebar .nav-link.active .feather { + color: inherit; +} + +.sidebar-heading { + font-size: .75rem; + text-transform: uppercase; +} + + +/* + * Navbar + */ + +.navbar-brand { + padding-top: .75rem; + padding-bottom: .75rem; + font-size: 1rem; + /*background-color: rgba(0, 0, 0, .25);*/ + /*box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25);*/ +} + +.navbar .navbar-toggler { + top: .25rem; + right: 1rem; +} + +.form-control { + transition: all 0.2s ease-in-out; +} + +.form-control:disabled { + cursor: not-allowed; +} + +.navbar .form-control { + padding: .75rem 1rem; + border-width: 0; + border-radius: 0; +} + +.form-control-dark { + color: #fff; + background-color: rgba(255, 255, 255, .1); + border-color: rgba(255, 255, 255, .1); +} + +.form-control-dark:focus { + border-color: transparent; + box-shadow: 0 0 0 3px rgba(255, 255, 255, .25); +} + +.dot { + width: 10px; + height: 10px; + border-radius: 50px; + display: inline-block; + margin-left: auto !important; +} + +.dot-running { + background-color: #28a745!important; + box-shadow: 0 0 0 0.2rem #28a74545; +} + +.h6-dot-running { + margin-left: 0.3rem; +} + +.dot-stopped { + background-color: #6c757d!important; +} + +.card-running { + border-color: #28a745; +} + +.info h6 { + line-break: anywhere; + transition: all 0.4s cubic-bezier(0.96, -0.07, 0.34, 1.01); + opacity: 1; +} + +.info .row .col-sm { + display: flex; + flex-direction: column; +} + +.info .row .col-sm small { + display: flex; +} + +.info .row .col-sm small strong:last-child(1) { + margin-left: auto !important; +} + +.btn-control { + border: none !important; + padding: 0; + margin: 0 1rem 0 0; +} + +.btn-control:hover{ + background-color: transparent !important; +} + +.btn-control:active, +.btn-control:focus { + background-color: transparent !important; + border: none !important; + box-shadow: none; +} + +.btn-qrcode-peer { + padding: 0 !important; +} + +.btn-qrcode-peer:active, +.btn-qrcode-peer:hover { + transform: scale(0.9) rotate(180deg); + border: 0 !important; +} + +.btn-download-peer:active, +.btn-download-peer:hover { + color: #17a2b8 !important; + transform: translateY(5px); +} + +.share_peer_btn_group .btn-control { + margin: 0 0 0 1rem; + padding: 0 !important; + transition: all 0.4s cubic-bezier(1, -0.43, 0, 1.37); +} + +.btn-control:hover { + background: white; +} + +.btn-delete-peer:hover { + color: #dc3545; +} + +.btn-lock-peer:hover { + color: #28a745; +} + +.btn-lock-peer.lock{ + color: #6c757d +} + +.btn-lock-peer.lock:hover{ + color: #6c757d +} + +.btn-control.btn-outline-primary:hover{ + color: #007bff +} + +/* .btn-setting-peer:hover { + color: #007bff +} */ + +.btn-download-peer:hover { + color: #17a2b8; +} + +.login-container { + padding: 2rem; +} + +@media (max-width: 992px) { + .card-col { + margin-bottom: 1rem; + } +} + +.switch { + font-size: 2rem; +} + +.switch:hover { + text-decoration: none +} + +.btn-group-label:hover { + color: #007bff; + border-color: #007bff; + background: white; +} + +.peer_data_group { + text-align: right; + display: flex; + margin-bottom: 0.5rem +} + +.peer_data_group p { + text-transform: uppercase; + margin-bottom: 0; + margin-right: 1rem +} + +@media (max-width: 768px) { + .peer_data_group { + text-align: left; + } +} + +.index-switch { + display: flex; + align-items: center; + justify-content: flex-end; +} + +main { + margin-bottom: 3rem; +} + +.peer_list { + margin-bottom: 7rem +} + +@media (max-width: 768px) { + .add_btn { + bottom: 1.5rem !important; + } + .peer_list { + margin-bottom: 7rem !important; + } +} + +.btn-manage-group { + z-index: 99; + position: fixed; + bottom: 3rem; + right: 2rem; + display: flex; +} + +.btn-manage-group .setting_btn_menu { + position: absolute; + top: -124px; + background-color: white; + padding: 1rem 0; + right: 0; + box-shadow: 0 10px 20px rgb(0 0 0 / 19%), 0 6px 6px rgb(0 0 0 / 23%); + border-radius: 10px; + min-width: 250px; + display: none; + transform: translateY(-30px); + opacity: 0; + transition: all 0.3s cubic-bezier(0.58, 0.03, 0.05, 1.28); +} + +.btn-manage-group .setting_btn_menu.show { + display: block; +} + +.setting_btn_menu.showing { + transform: translateY(0px); + opacity: 1; +} + +.setting_btn_menu a { + display: flex; + padding: 0.5rem 1rem; + transition: all 0.1s ease-in-out; + font-size: 1rem; + align-items: center; + cursor: pointer; +} + +.setting_btn_menu a:hover { + background-color: #efefef; + text-decoration: none; +} + +.setting_btn_menu a i { + margin-right: auto !important; +} + +.add_btn { + height: 54px; + z-index: 99; + border-radius: 100px !important; + padding: 0 14px; + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23); + margin-right: 1rem; + font-size: 1.5rem; +} + +.setting_btn { + height: 54px; + z-index: 99; + border-radius: 100px !important; + padding: 0 14px; + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23); + font-size: 1.5rem; +} + +@-webkit-keyframes rotating +/* Safari and Chrome */ + +{ + from { + -webkit-transform: rotate(0deg); + -o-transform: rotate(0deg); + transform: rotate(0deg); + } + to { + -webkit-transform: rotate(360deg); + -o-transform: rotate(360deg); + transform: rotate(360deg); + } +} + +@keyframes rotating { + from { + -ms-transform: rotate(0deg); + -moz-transform: rotate(0deg); + -webkit-transform: rotate(0deg); + -o-transform: rotate(0deg); + transform: rotate(0deg); + } + to { + -ms-transform: rotate(360deg); + -moz-transform: rotate(360deg); + -webkit-transform: rotate(360deg); + -o-transform: rotate(360deg); + transform: rotate(360deg); + } +} + +.rotating::before { + -webkit-animation: rotating 0.75s linear infinite; + -moz-animation: rotating 0.75s linear infinite; + -ms-animation: rotating 0.75s linear infinite; + -o-animation: rotating 0.75s linear infinite; + animation: rotating 0.75s linear infinite; +} + +.peer_private_key_textbox_switch { + position: absolute; + right: 2rem; + transform: translateY(-28px); + font-size: 1.2rem; + cursor: pointer; +} + +#peer_private_key_textbox, +#private_key, +#public_key, +#peer_preshared_key_textbox { + font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; +} + +.progress-bar { + transition: 0.3s ease-in-out; +} + +.key { + transition: 0.2s ease-in-out; + cursor: pointer; +} + +.key:hover { + color: #007bff; +} + +.card { + border-radius: 10px; +} + +.peer_list .card .button-group { + height: 22px; +} + +.form-control { + border-radius: 10px; +} + +.btn { + border-radius: 8px; + /*padding: 0.6rem 0.9em;*/ +} + +.login-box #username, +.login-box #password { + padding: 0.6rem calc( 0.9rem + 32px); + height: inherit; +} + +.login-box label[for="username"], +.login-box label[for="password"] { + font-size: 1rem; + margin: 0 !important; + transform: translateY(2.1rem) translateX(1rem); + padding: 0; +} + + +/*label[for="password"]{*/ + + +/* transform: translateY(32px) translateX(16px);*/ + + +/*}*/ + +.modal-content { + border-radius: 10px; +} + +.tooltip-inner { + font-size: 0.8rem; +} + +@-webkit-keyframes loading { + 0% { + background-color: #dfdfdf; + } + 50% { + background-color: #adadad; + } + 100% { + background-color: #dfdfdf; + } +} + +@-moz-keyframes loading { + 0% { + background-color: #dfdfdf; + } + 50% { + background-color: #adadad; + } + 100% { + background-color: #dfdfdf; + } +} + +.conf_card { + transition: 0.2s ease-in-out; +} + +.conf_card:hover { + border-color: #007bff; + cursor: pointer; +} + +.info_loading { + /* animation: loading 2s infinite ease-in-out; + /* border-radius: 5px; */ + height: 19.19px; + /* transition: 0.3s ease-in-out; */ + + /* transform: translateX(40px); */ + opacity: 0 !important; +} + +#conf_status_btn { + transition: 0.2s ease-in-out; +} + +#conf_status_btn.info_loading { + height: 38px; + border-radius: 5px; + animation: loading 3s infinite ease-in-out; +} + +#qrcode_img img { + width: 100%; +} + +#selected_ip_list .badge, +#selected_peer_list .badge { + margin: 0.1rem +} + +#add_modal.ip_modal_open { + transition: filter 0.2s ease-in-out; + filter: brightness(0.5); +} + +#delete_bulk_modal .list-group a.active { + background-color: #dc3545; + border-color: #dc3545; +} + +#selected_peer_list { + max-height: 80px; + overflow-y: scroll; + overflow-x: hidden; +} + +.no-response { + width: 100%; + height: 100%; + position: fixed; + background: #000000ba; + z-index: 10000; + display: none; + flex-direction: column; + align-items: center; + justify-content: center; + opacity: 0; + transition: all 1s ease-in-out; +} + +.no-response.active { + display: flex; +} + +.no-response.active.show { + opacity: 100; +} + +.no-response .container>* { + text-align: center; +} + +.no-responding { + transition: all 1s ease-in-out; + filter: blur(10px); +} + +pre.index-alert { + margin-bottom: 0; + padding: 1rem; + background-color: #343a40; + border: 1px solid rgba(0, 0, 0, .125); + border-radius: .25rem; + margin-top: 1rem; + color: white; +} + +.peerNameCol { + display: flex; + align-items: center; + margin-bottom: 0.2rem +} + +.peerName { + margin: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.peerLightContainer { + text-transform: uppercase; + margin: 0; + margin-left: auto !important; +} + +.conf_card .dot, +.info .dot { + transform: translateX(10px); +} + +#config_body { + transition: 0.3s ease-in-out; +} + + +#config_body.firstLoading { + opacity: 0.2; +} + +.chartTitle { + display: flex; +} + +.chartControl { + margin-bottom: 1rem; + display: flex; + align-items: center; +} + +.chartTitle h6 { + margin-bottom: 0; + line-height: 1; + margin-right: 0.5rem; +} + +.chartContainer.fullScreen { + position: fixed; + z-index: 9999; + background-color: white; + top: 0; + left: 0; + width: calc( 100% + 15px); + height: 100%; + padding: 32px; +} + +.chartContainer.fullScreen .col-sm { + padding-right: 0; + height: 100%; +} + +.chartContainer.fullScreen .chartCanvasContainer { + width: 100%; + height: calc( 100% - 47px) !important; + max-height: calc( 100% - 47px) !important; +} + +#switch{ + transition: all 200ms ease-in; +} + +.toggle--switch{ + display: none; +} + +.toggleLabel{ + width: 64px; + height: 32px; + background-color: #6c757d17; + display: flex; + position: relative; + border: 2px solid #6c757d8c; + border-radius: 100px; + transition: all 200ms ease-in; + cursor: pointer; + margin: 0; +} + +.toggle--switch.waiting + .toggleLabel{ + opacity: 0.5; +} + +.toggleLabel::before{ + background-color: #6c757d; + height: 26px; + width: 26px; + content: ""; + border-radius: 100px; + margin: 1px; + position: absolute; + animation-name: off; + animation-duration: 350ms; + animation-fill-mode: forwards; + transition: all 200ms ease-in; + cursor: pointer; +} + +.toggleLabel:hover::before{ + filter: brightness(1.2); +} + + +.toggle--switch:checked + .toggleLabel{ + background-color: #007bff17 !important; + border: 2px solid #007bff8c; +} + +.toggle--switch:checked + .toggleLabel::before{ + background-color: #007bff; + animation-name: on; + animation-duration: 350ms; + animation-fill-mode: forwards; +} + +@keyframes on { + 0%{ + left: 0px; + } + 60%{ + left: 0px; + width: 40px; + } + 100%{ + left: 32px; + width: 26px; + } +} + +@keyframes off { + 0%{ + left: 32px; + } + 60%{ + left: 18px; + width: 40px; + } + 100%{ + left: 0px; + width: 26px; + } +} + +.toastContainer{ + z-index: 99999 !important; +} + +.toast{ + min-width: 300px; + background-color: rgba(255,255,255,1); + z-index: 99999; +} + +.toast-header{ + background-color: rgba(255,255,255); +} + +.toast-progressbar{ + width: 100%; + height: 4px; + background-color: #007bff; + border-bottom-left-radius: .25rem; +} + +.addConfigurationAvailableIPs{ + margin-bottom: 0; +} + +.input-feedback{ + display: none; +} + +#addConfigurationModal label{ + display: flex; + width: 100%; + align-items: center; +} + +#addConfigurationModal label a{ + margin-left: auto !important; +} + +#reGeneratePrivateKey{ + border-top-right-radius: 10px; + border-bottom-right-radius: 10px; +} + +.addConfigurationToggleStatus.waiting{ + opacity: 0.5; +} + +/*.conf_card .card-body .row .card-col{*/ +/* margin-bottom: 0.5rem;*/ +/*}*/ + +.peerDataUsageChartContainer{ + min-height: 50vh; + width: 100%; +} + +.peerDataUsageChartControl{ + display: block !important; + margin: 0; +} + +.peerDataUsageChartControl .switchUnit{ + width: 33.3%; +} + +.peerDataUsageChartControl .switchTimePeriod{ + width: 25%; +} + +@media (min-width: 1200px){ + #peerDataUsage .modal-xl { + max-width: 95vw; + } +} + +.bottom{ + display: none; +} + + +@media (max-width: 768px){ + .bottom{ + display: block; + } + + .btn-manage-group{ + bottom: calc( 3rem + 40px + env(safe-area-inset-bottom, 5px)); + } + + main{ + padding-bottom: calc( 3rem + 40px + env(safe-area-inset-bottom, 5px)); + } +} + + +.bottomNavContainer{ + display: flex; + color: #333; + padding-bottom: env(safe-area-inset-bottom, 5px); + box-shadow: inset 0 1px 0 rgb(0 0 0 / 10%); +} + +.bottomNavButton{ + width: 25vw; + display: flex; + flex-direction: column; + align-items: center; + margin: 0.7rem 0; + color: rgba(51, 51, 51, 0.5); + cursor: pointer; + transition: all ease-in 0.2s; +} + +.bottomNavButton.active{ + color: #333; +} + +.bottomNavButton i{ + font-size: 1.2rem; +} + +.bottomNavButton .subNav{ + width: 100vw; + position: absolute; + z-index: 10000; + bottom: 0; + left: 0; + background-color: #272b30; + display: none; + animation-duration: 400ms; + padding-bottom: env(safe-area-inset-bottom, 5px); +} + +.bottomNavButton .subNav.active{ + display: block; +} + + +.bottomNavButton .subNav .nav .nav-item .nav-link{ + padding: 0.7rem 1rem; +} + +.bottomNavWrapper{ + height: 100%; + width: 100%; + background-color: #000000a1; + position: fixed; + z-index: 1030; + display: none; + left: 0; +} + +.bottomNavWrapper.active{ + display: block; +} + +.sb-update-url .dot-running{ + transform: translateX(10px); +} + +.list-group-item{ + transition: all 0.1s ease-in; +} + +.theme-switch-btn{ + width: 100%; +} \ No newline at end of file diff --git a/src/static/app/src/App.vue b/src/static/app/src/App.vue index c0c4ebe..10bc14e 100644 --- a/src/static/app/src/App.vue +++ b/src/static/app/src/App.vue @@ -1,9 +1,10 @@