diff --git a/src/config.ini b/src/config.ini index 1ede5a4..8305a50 100644 --- a/src/config.ini +++ b/src/config.ini @@ -1,4 +1,4 @@ -[Peers] +[peers] remote_endpoint = 89.20.90.254 peer_global_dns = 9.9.9.9 peer_endpoint_allowed_ip = 0.0.0.0/0 @@ -6,59 +6,59 @@ peer_display_mode = grid peer_mtu = 1420 peer_keep_alive = 21 -[Server] +[server] hostname = 0.0.0.0 port = 10086 debug_enabled = True wg_conf_path = /etc/wireguard awg_conf_path = /etc/amnezia/amneziawg -app_prefix = +app_prefix = authentication_required = True version = v5.0.0 wgdashboard_refresh_interval = 60000 wgdashboard_peer_list_display = grid wgdashboard_sort = status wgdashboard_theme = dark -wgdashboard_apikey = true -#wgdashboard_language = nl-NL +wgdashboard_apikey = True wgdashboard_language = en-US log_level = DEBUG -[Account] -username = admin -password = $2b$12$1VN62Q7CS/BJcAahHAWsA.3CD6zPqWTmE/HN/AJqwP0zds2l25Fqe -enable_totp = false -totp_verified = false -totp_key = ZSI2Z4QHSGVAK6TFVMZXORFDWKHDN4TQ +[account] +username = dselen +password = $2b$12$bUcVL2v0OoJnraXtx.mHsOKFRnKEf/kmewqKXHOebitDZcJviPusm +enable_totp = False +totp_verified = False +totp_key = CEBHHGL4HLYUDYXKTLIL34SVLXYPVHIO -[Other] -welcome_session = false +[other] +welcome_session = False -[Database] +[database] type = sqlite -host = -port = -username = -password = +host = +port = +username = +password = -[Email] -server = -port = -encryption = -username = -email_password = -authentication_required = true -send_from = -email_template = +[email] +server = +port = +encryption = +username = +email_password = +authentication_required = True +send_from = +email_template = -[OIDC] -admin_enable = false -client_enable = false +[oidc] +admin_enable = False +client_enable = False -[Clients] -enable = true -sign_up = true +[clients] +enable = True +sign_up = True + +[wireguardconfiguration] +autostart = +peer_tracking = False -[WireGuardConfiguration] -autostart = -peer_tracking = false \ No newline at end of file diff --git a/src/main.py b/src/main.py index 6098f12..87680aa 100644 --- a/src/main.py +++ b/src/main.py @@ -15,20 +15,21 @@ from modules.utilities.utilities import utilities as util from modules.utilities.logger import setup_logger from modules.routes.routes import routes +from modules.routes.routes_welcome import routes_welcome if __name__ == '__main__': # Read the config file (ini) - ok, config_contents = config.read() + ok, config_data = config.read() if not ok: exit(1) - found, config_server = config.filter(config_contents, 'SERVER') + found, config_server = config.filter(config_data, 'SERVER') # Configure the loglevel of WGDashboard wanted_loglevel = config_server.get('log_level', 'DEBUG').upper() setup_logger(wanted_loglevel) # Get the database configuration from thee config - found, config_database = config.filter(config_contents, 'DATABASE') + found, config_database = config.filter(config_data, 'DATABASE') if not found: exit(1) @@ -40,13 +41,14 @@ if __name__ == '__main__': # Configure the Flask app app = flask.Flask("WGDashboard", - static_url_path="", + static_url_path=prefix, template_folder=os.path.abspath("./static/dist/WGDashboardAdmin"), static_folder=os.path.abspath("./static/dist/WGDashboardAdmin") ) app.register_blueprint(routes, url_prefix=prefix) + app.register_blueprint(routes_welcome, url_prefix=prefix) - app.wgd_config = config_contents + app.wgd_config = config_data app.locale_path = './static/locales/' app.secret_key = secrets.token_urlsafe(64) diff --git a/src/modules/config/config.py b/src/modules/config/config.py index a32f31a..1d42653 100644 --- a/src/modules/config/config.py +++ b/src/modules/config/config.py @@ -1,19 +1,24 @@ #!/bin/env python3 import logging as log +import flask -from .utilities import config_utilities +from .config_utils import config_utils class config(): @staticmethod - def filter(config_contents: dict, filter_keyword: str) -> tuple[bool, dict]: + def filter(config_data: dict, filter_keyword: str) -> tuple[bool, dict]: ''' Helper function to grab a specific part of the config ''' - for section_name, section_values in config_contents.items(): - if str(section_name).lower() == filter_keyword.lower(): + lower_filter_keyword = filter_keyword.lower() + + for section_name, section_values in config_data.items(): + if str(section_name).lower() == lower_filter_keyword: if isinstance(section_values, dict): return True, dict(section_values) + + log.error("failed to properly filter the config") return False, {} @staticmethod @@ -22,17 +27,37 @@ class config(): check some basic things and then return the dict containing the config data ''' - ok, candidate_path = config_utilities.search_known_paths() + ok, candidate_path = config_utils.search_known_paths() if not ok: + log.error("failed to retrieve a valid path for the config") return False, {} - ok, config_contents = config_utilities.verify_contents(candidate_path) + + ok, config_data = config_utils.read_data(candidate_path) if not ok: return False, {} - return True, config_contents - + return True, config_data + @staticmethod - def update(section: str, key: str, value: str) -> bool: - print(config_utilities.search_known_paths()) - print(section, key, value) - return True + def update(target_section: str, target_key: str, new_value) -> bool: + lower_target_section = target_section.lower() + lower_target_key = target_key.lower() + + ok, config_data = config.read() + if not ok: + log.error("failed to read the config succesfully") + return False + + if not config_utils.find_section(config_data, lower_target_section): + log.error("failed to find the given section") + return False + if not config_utils.find_key(config_data, lower_target_section, lower_target_key): + log.error("failed to find the given key in the given section") + return False + + ok = config_utils.write_key(config_data, lower_target_section, lower_target_key, new_value) + if not ok: + log.error("failed to write the key to the file") + return False + + return True \ No newline at end of file diff --git a/src/modules/config/utilities.py b/src/modules/config/utilities.py deleted file mode 100644 index e85d3b3..0000000 --- a/src/modules/config/utilities.py +++ /dev/null @@ -1,69 +0,0 @@ -#!/bin/env python3 - -import logging as log - -import configparser as cp -import os - -class config_utilities(): - @staticmethod - def search_known_paths() -> tuple[bool, str]: - ''' - Look at predefined paths on the filesystem for a config file - ''' - - possible_config_locations = [ - "./config.ini", - f"{os.getenv('HOME')}/.config/wgdashboard/config.ini", - "/etc/wgdashboard/config.ini", - ] - - try: - for path in possible_config_locations: - if os.path.exists(path): - return True, path - else: - continue - return False, '' - - except Exception as err: - return False, '' - - @staticmethod - def verify_contents(config_path: str) -> tuple[bool, dict]: - ''' - Check the existing config file for contents - ''' - config = cp.ConfigParser() - - try: - config.read(config_path) - - if len(config.sections()) == 0: - return False, {} - - for section in config.sections(): - if len(config.items(section)) == 0: - config.remove_section(section) - - config_dict = {} - for section in config.sections(): - items = dict(config.items(section)) - - for key, value in items.items(): - if value.strip().lower() == 'true': - items[key] = True - elif value.strip().lower() == 'false': - items[key] = False - else: - items[key] = value - - config_dict[section] = items - - return True, config_dict - - except cp.ParsingError as err: - return False, {} - - except Exception as err: - return False, {} \ No newline at end of file diff --git a/src/modules/database/database.py b/src/modules/database/database.py index c166a14..a426c5b 100644 --- a/src/modules/database/database.py +++ b/src/modules/database/database.py @@ -6,12 +6,12 @@ import sqlalchemy import sqlalchemy.orm from .schema import Base -from .utilities import checks +from .database_utils import database_utils class database(): @staticmethod def create_session(database_config: dict) -> tuple[bool, sqlalchemy.engine.Engine | None, sqlalchemy.orm.Session | None]: - ok, connection_string = checks.generate_connection_string(database_config) + ok, connection_string = database_utils.generate_connection_string(database_config) if not ok: return False, None, None diff --git a/src/modules/database/functions.py b/src/modules/database/functions.py index 9b91270..35375c6 100644 --- a/src/modules/database/functions.py +++ b/src/modules/database/functions.py @@ -6,7 +6,7 @@ import sqlalchemy.orm import json from .schema import Base -from .schema import Apikeys +from .schema import Apikeys, User class functions(): @staticmethod @@ -63,5 +63,8 @@ class functions(): return valid_keys, expired_keys @staticmethod - def retrieve_users(session: sqlalchemy.orm.Session): - print("Wanting to check users") \ No newline at end of file + def retrieve_user_objects(session: sqlalchemy.orm.Session) -> list[dict]: + stored_users = session.query(User).all() + + for user in stored_users: + print(user) \ No newline at end of file diff --git a/src/modules/database/schema.py b/src/modules/database/schema.py index 4013bad..d622bf5 100644 --- a/src/modules/database/schema.py +++ b/src/modules/database/schema.py @@ -17,7 +17,7 @@ class User(Base): totp_enabled = sqlalchemy.Column(sqlalchemy.Boolean, default=False) totp_verified = sqlalchemy.Column(sqlalchemy.Boolean, default=False) - totp_key = sqlalchemy.Column(sqlalchemy.String) + totp_secret = sqlalchemy.Column(sqlalchemy.String) email = sqlalchemy.Column(sqlalchemy.String) diff --git a/src/modules/database/utilities.py b/src/modules/database/utilities.py deleted file mode 100644 index b99eea9..0000000 --- a/src/modules/database/utilities.py +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/env python3 - -import logging as log - -import os -from urllib.parse import quote_plus - -from modules.utilities.utilities import utilities as util - -class checks(): - ''' - This class functions as a collection of function to prepare a connection to a database. - ''' - - @staticmethod - def generate_connection_string(database_config: dict) -> tuple[bool, str]: - if not 'type' in database_config: - return False, '' - - username = quote_plus(database_config.get('username', '')) - password = quote_plus(database_config.get('password', '')) - - match database_config['type']: - case 'sqlite': - local_database_path = os.path.abspath("./database") - - exists = util.ensure_directory(local_database_path) - if exists: - connection_string = f'sqlite:///{local_database_path}/wgdashboard.db' - else: - return False, '' - - case 'postgresql' | 'postgres': - connection_string = f'postgresql+psycopg://{username}:{password}@{database_config.get('host', 'localhost')}:{database_config.get('port', '5432')}' - - case 'mariadb': - connection_string = f'mariadb+mariadbconnector://{username}:{password}@{database_config.get('host', 'localhost')}:{database_config.get('port', '3306')}' - - case 'mysql': - connection_string = f'mysql+pymysql://{username}:{password}@{database_config.get('host', 'localhost')}:{database_config.get('port', '3306')}' - - case _: - return False, '' - - return True, connection_string \ No newline at end of file diff --git a/src/modules/routes/routes.py b/src/modules/routes/routes.py index ba2bc60..fcae848 100644 --- a/src/modules/routes/routes.py +++ b/src/modules/routes/routes.py @@ -11,7 +11,7 @@ import werkzeug from datetime import datetime from .response import make_resp_obj -from .utilities import helpers +from .routes_utils import routes_utils from .locale import localeman from ..database.functions import functions @@ -20,19 +20,10 @@ from ..config.config import config routes = flask.Blueprint("routes", __name__) white_list = [ - "/", # we need to whitelist / - "/client", - "/static/", - "/fileDownload", - "/api/authenticate", - "/api/locale", - "getDashboardConfiguration", - "getDashboardTheme", - "getDashboardVersion", - "sharePeer/get", - "isTotpEnabled", - "validateAuthentication", - "favicon.ico", + "/", "/client", "/static/", "/fileDownload", + "/api/authenticate", "/api/locale", "getDashboardConfiguration", + "getDashboardTheme", "getDashboardVersion", "sharePeer/get", + "isTotpEnabled", "validateAuthentication", "favicon.ico", ] @routes.before_request @@ -42,6 +33,7 @@ def authentication_required(): ok, config_server = config.filter(flask.current_app.wgd_config, 'SERVER') if not ok: + log.error("failed to filter the config in-memory") return make_resp_obj(False, "Internal error", {}, 500) auth_required_flag = config_server.get('auth_req', True) @@ -54,7 +46,7 @@ def authentication_required(): api_key = flask.request.headers.get("wgdashboard-apikey") if api_key and api_key_enabled: - if helpers.is_valid_api_key(api_key): + if routes_utils.is_valid_api_key(api_key): return else: return make_resp_obj(False, "WGDashboard API-key does not exist or is invalid/expired", {}, 401) @@ -62,7 +54,7 @@ def authentication_required(): if flask.session.get("role") == "admin": return - if helpers.is_path_allowed(path, white_list, flask.session): + if routes_utils.is_path_allowed(path, white_list, flask.session): return return make_resp_obj(False, "Unauthorized access", {}, 401) @@ -71,6 +63,7 @@ def authentication_required(): def api_authenticate(): ok, config_server = config.filter(flask.current_app.wgd_config, 'SERVER') if not ok: + log.error("failed to filter the config in-memory") return make_resp_obj(False, "Internal error", {}, 500) auth_required_flag = config_server.get('authentication_required', True) @@ -79,23 +72,25 @@ def api_authenticate(): if not auth_required_flag: ok, config_other = config.filter(flask.current_app.wgd_config, 'OTHER') if not ok: + log.error("failed to filter the config in-memory") return make_resp_obj(False, "Internal error", {}, 500) - return make_resp_obj(True, "Login successful, no authentication required", {"welcome_session": config_other.get("welcome_session", False)}, 200) + welcome_session_enabled = config_other.get("welcome_session", False) + return make_resp_obj(True, "Login successful, no authentication required", {"welcome_session": welcome_session_enabled}, 200) # API key authentication - api_key = flask.request.headers.get("wgdashboard-apikey") + given_api_key = flask.request.headers.get("wgdashboard-apikey") api_key_enabled = config_server.get("wgdashboard_apikey", False) - if api_key and api_key_enabled: - if helpers.is_valid_api_key(api_key): - auth_token = hashlib.sha256(f"{api_key}{datetime.now()}".encode()).hexdigest() + if given_api_key and api_key_enabled: + if routes_utils.is_valid_api_key(given_api_key): + authentication_token = hashlib.sha256(f"{given_api_key}{datetime.now()}".encode()).hexdigest() flask.session['role'] = 'admin' - flask.session['username'] = auth_token + flask.session['username'] = authentication_token resp = make_resp_obj(True,"Login successful", {}, 200) - resp.set_cookie("authToken", auth_token) + resp.set_cookie("authToken", authentication_token) flask.session.permanent = True return resp else: @@ -104,15 +99,19 @@ def api_authenticate(): # Load account config ok, config_account = config.filter(flask.current_app.wgd_config, 'ACCOUNT') if not ok: + log.error("failed to filter the config in-memory") return make_resp_obj(False, "Internal error", {}, 500) - data = flask.request.get_json() - if not data: + req_data = flask.request.get_json() + if not req_data: return make_resp_obj(False, "Invalid request body", {}, 400) - username = data.get("username") - password = data.get("password") - totp_code = data.get("totp") + username = req_data.get("username") + password = req_data.get("password") + totp_code = req_data.get("totp") + + #stored_user_objects = functions.retrieve_user_objects(flask.current_app.db_session) + #print(stored_user_objects) stored_username = config_account.get("username") stored_password = config_account.get("password") @@ -136,10 +135,10 @@ def api_authenticate(): and ((totp_enabled and totp_valid) or not totp_enabled) ): # Generate a session token - auth_token = hashlib.sha256(f"{username}{datetime.now()}".encode()).hexdigest() + authentication_token = hashlib.sha256(f"{username}{datetime.now()}".encode()).hexdigest() flask.session['role'] = 'admin' - flask.session['username'] = auth_token + flask.session['username'] = authentication_token flask.session.permanent = True # Log success via your helper if available @@ -149,7 +148,7 @@ def api_authenticate(): welcome_msg = config_other.get("welcome_session", "Welcome back!") if ok else "Welcome!" resp = make_resp_obj(True, {"status": True}, 200) - resp.set_cookie("authToken", auth_token, httponly=True, samesite='Lax') + resp.set_cookie("authToken", authentication_token, httponly=True, samesite='Lax') return resp # Log failure @@ -188,6 +187,7 @@ def api_locale_handler(): def api_validate_auth(): ok, config_server = config.filter(flask.current_app.wgd_config, 'SERVER') if not ok: + log.error("failed to filter the config in-memory") return make_resp_obj(False, 'Internal error', {}, 500) auth_required_flag = config_server.get('auth_req', True) @@ -202,6 +202,7 @@ def api_validate_auth(): def api_retrieve_dashboard_version(): ok, config_server = config.filter(flask.current_app.wgd_config, 'SERVER') if not ok: + log.error("failed to filter the config in-memory") return make_resp_obj(False, 'Internal error', {}, 500) return make_resp_obj(True, "", config_server.get("version")) @@ -210,6 +211,7 @@ def api_retrieve_dashboard_version(): def api_retrieve_dashboard_theme(): ok, config_server = config.filter(flask.current_app.wgd_config, 'SERVER') if not ok: + log.error("failed to filter the config in-memory") return make_resp_obj(False, 'Internal error', {}, 500) return make_resp_obj(True, "", config_server.get("wgdashboard_theme"), 200) @@ -222,6 +224,7 @@ def api_retrieve_dashboard_config(): def api_totp_status(): ok, config_account = config.filter(flask.current_app.wgd_config, 'ACCOUNT') if not ok: + log.error("failed to filter the config in-memory") return make_resp_obj(False, 'Internal error', {}, 500) data = config_account.get('enable_totp') and config_account.get('totp_verified') diff --git a/src/modules/routes/utilities.py b/src/modules/routes/utilities.py deleted file mode 100644 index ffd90a3..0000000 --- a/src/modules/routes/utilities.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/env python3 - -import flask - -from ..database.functions import functions - -class helpers(): - @staticmethod - def is_valid_api_key(api_key: str) -> bool: - valid_keys, _ = functions.retrieve_api_keys(flask.current_app.db_session) - valid_key_strings = [] - - for key in valid_keys: - valid_key_strings.append(key["key"]) - - return api_key in valid_key_strings - - @staticmethod - def is_path_allowed(path: str, white_list: list[str], session_data: dict) -> bool: - # Allow if session has admin role - if session_data.get("role") == "admin" and "username" in session_data: - return True - # Check white list - for p in white_list: - if p in path: - return True - return False \ No newline at end of file diff --git a/src/modules/wireguard/utilities.py b/src/modules/wireguard/utilities.py deleted file mode 100644 index 92bc4e7..0000000 --- a/src/modules/wireguard/utilities.py +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/env python3 - diff --git a/src/requirements.txt b/src/requirements.txt index 451afd5..aeed4ce 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -3,4 +3,5 @@ configparser==7.2.0 gunicorn==25.1.0 sqlalchemy==2.0.48 Flask==3.1.3 -Werkzeug==3.1.6 \ No newline at end of file +Werkzeug==3.1.6 +pyotp==2.9.0 \ No newline at end of file diff --git a/src/static/admin/src/App.vue b/src/static/admin/src/App.vue index e682488..ad06f24 100644 --- a/src/static/admin/src/App.vue +++ b/src/static/admin/src/App.vue @@ -29,7 +29,7 @@ const route = useRoute()