From 3f276a42e1522446db27def15dada5bf6159bf0e Mon Sep 17 00:00:00 2001 From: Scott Idem Date: Thu, 15 Jan 2026 16:59:18 -0500 Subject: [PATCH] Refactor: Modularize configuration and implement robust DB bootstrap --- app/db_sql.py | 30 +++++++++++++++ app/lib_config_v3.py | 87 ++++++++++++++++++++++++++++++++++++++++++++ app/main.py | 52 ++++++++++---------------- 3 files changed, 136 insertions(+), 33 deletions(-) create mode 100644 app/lib_config_v3.py diff --git a/app/db_sql.py b/app/db_sql.py index 3c0728d..d13f964 100644 --- a/app/db_sql.py +++ b/app/db_sql.py @@ -67,6 +67,36 @@ try: except Exception: log.exception('Could not connect to database.') +def reconnect_db() -> bool: + """ + Re-initializes the global database engine and connection using current settings. + Useful after bootstrapping new credentials from the 'cfg' table. + """ + global engine, db, db_uri + + log.info("Refreshing database connection engine...") + try: + if engine: + engine.dispose() + log.info("Disposed of previous database engine.") + + db_uri = settings.SQLALCHEMY_DB_URI + engine = create_engine( + url = db_uri, + echo = False, + pool_use_lifo = True, + pool_pre_ping = True, + pool_recycle = settings.DB['pool_recycle'], + isolation_level = 'READ COMMITTED', + connect_args = {'connect_timeout': settings.DB['connect_timeout']} + ) + db = engine.connect() + log.info(f"Database connection re-established successfully: {db_uri}") + return True + except Exception: + log.exception("FAILED to refresh database connection!") + return False + # ### BEGIN ### API DB SQL ### sql_connect() ### @logger_reset def sql_connect(current_db, log_lvl: int = logging.INFO) -> None|bool|int: diff --git a/app/lib_config_v3.py b/app/lib_config_v3.py new file mode 100644 index 0000000..d9c169e --- /dev/null +++ b/app/lib_config_v3.py @@ -0,0 +1,87 @@ +import logging +from typing import Any + +log = logging.getLogger('root') + +def validate_critical_config(settings: Any): + """ + Validates that essential settings are populated and not using placeholders. + Logs warnings or errors for missing critical infrastructure. + """ + log.info("Checking critical system configuration...") + + # 1. Database Check + db = getattr(settings, 'DB', {}) + if not db.get('server') or db.get('server') == 'mariadb': + # 'mariadb' is the default in .env, usually fine, but worth noting + log.info(f"Database server: {db.get('server')}") + + # 2. SMTP Check + smtp = getattr(settings, 'SMTP', {}) + if not smtp.get('server'): + log.warning("CRITICAL: SMTP server not configured. Email features will fail.") + if smtp.get('password') == 'set-in-ae-sql-db-cnf-tbl': + log.error("CRITICAL: SMTP password is still set to placeholder. Email authentication will fail.") + + # 3. Security Check + jwt_key = getattr(settings, 'JWT_KEY', '') + if not jwt_key or jwt_key == 'fake-super-secret-token': + log.error("SECURITY: JWT_KEY is missing or using a known fake token!") + + log.info("Configuration validation complete.") + +def bootstrap_db_config(settings: Any) -> bool: + """ + Loads dynamic settings from the 'cfg' table and updates the settings object. + Uses deferred import of sql_select to avoid circular dependencies. + """ + # CRITICAL: Deferred import to prevent boot-time circular dependencies + from app.db_sql import sql_select + + cfg_id = settings.AETHER_CFG.get('id', '0') + log.info(f"Bootstrapping system configuration from DB (cfg_id={cfg_id})...") + + try: + # Fetch the config record + aether_cfg_sql = sql_select( + table_name='cfg', + record_id=int(cfg_id), + as_list=False, + max_count=1, + ) + + # In some cases sql_select might return a single-item list even with as_list=False + if isinstance(aether_cfg_sql, list): + if len(aether_cfg_sql) > 0: + aether_cfg_sql = aether_cfg_sql[0] + else: + aether_cfg_sql = None + + if not aether_cfg_sql or not isinstance(aether_cfg_sql, dict): + log.error(f"FAILED to load system config from DB for ID {cfg_id}. Table 'cfg' might be empty or ID missing.") + return False + + # --- Update Database settings --- + # Safety: Only update if the values are provided in the DB record + if aether_cfg_sql.get('db_server'): settings.DB_SERVER = aether_cfg_sql.get('db_server') + if aether_cfg_sql.get('db_port'): settings.DB_PORT = str(aether_cfg_sql.get('db_port')) + if aether_cfg_sql.get('db_name'): settings.DB_NAME = aether_cfg_sql.get('db_name') + if aether_cfg_sql.get('db_username'): settings.DB_USER = aether_cfg_sql.get('db_username') + if aether_cfg_sql.get('db_password'): settings.DB_PASS = aether_cfg_sql.get('db_password') + + # --- Update SMTP Settings --- + if aether_cfg_sql.get('smtp_server'): settings.SMTP['server'] = aether_cfg_sql.get('smtp_server') + if aether_cfg_sql.get('smtp_port'): settings.SMTP['port'] = str(aether_cfg_sql.get('smtp_port')) + if aether_cfg_sql.get('smtp_username'): settings.SMTP['username'] = aether_cfg_sql.get('smtp_username') + if aether_cfg_sql.get('smtp_password'): settings.SMTP['password'] = aether_cfg_sql.get('smtp_password') + + # --- Update File Paths --- + if aether_cfg_sql.get('path_hosted_files_root'): settings.FILES_PATH['hosted_files_root'] = aether_cfg_sql.get('path_hosted_files_root') + if aether_cfg_sql.get('path_hosted_tmp_root'): settings.FILES_PATH['hosted_tmp_root'] = aether_cfg_sql.get('path_hosted_tmp_root') + + log.info("System configuration successfully synchronized with DB.") + return True + + except Exception as e: + log.exception(f"Unexpected error during system bootstrap: {e}") + return False diff --git a/app/main.py b/app/main.py index c4543d9..a0ed11f 100644 --- a/app/main.py +++ b/app/main.py @@ -21,7 +21,8 @@ from app.middleware import add_process_time_header as process_time_middleware # Centralized router registry from app.routers.registry import setup_routers -from app.db_sql import sql_select, reset_redis # , sql_connect +from app.db_sql import sql_select, reset_redis, reconnect_db +from app.lib_config_v3 import bootstrap_db_config, validate_critical_config print('### **** *** ** * The Aether API v4 using FastAPI is loading... * ** *** **** ###') @@ -47,41 +48,26 @@ app = FastAPI( log.setLevel(logging.INFO) # log.debug(config.settings) -print('### **** *** ** * Aether API v4 using FastAPI - About to try first SQL SELECT (sql_select()) while loading... * ** *** **** ###') -if aether_cfg_sql_result := sql_select( - table_name = 'cfg', - record_id = config.settings.AETHER_CFG['id'], - as_list = False, - max_count = 1, - ): - aether_cfg_sql = aether_cfg_sql_result +print('### **** *** ** * Aether API v4 using FastAPI - Bootstrapping Configuration... * ** *** **** ###') - config.settings.DB['server'] = aether_cfg_sql.get('db_server') - config.settings.DB['port'] = aether_cfg_sql.get('db_port') - config.settings.DB['name'] = aether_cfg_sql.get('db_name') - config.settings.DB['username'] = aether_cfg_sql.get('db_username') - config.settings.DB['password'] = aether_cfg_sql.get('db_password') +# Sync settings from DB with robust error handling +try: + if bootstrap_db_config(config.settings): + log.info("Successfully bootstrapped configuration from database.") + # Re-initialize the database engine with new credentials/URI + if reconnect_db(): + log.info("Database connection re-established with production configuration.") + else: + log.warning("FAILED to re-establish database connection after bootstrap. Falling back to .env settings.") + else: + log.warning("System bootstrap from DB returned no results. Using environment defaults.") +except Exception as e: + log.error(f"Unexpected error during configuration bootstrap: {e}. Falling back to .env settings.") - DB = config.settings.DB - config.settings.SQLALCHEMY_DB_URI = 'mysql://'+DB['username']+':'+DB['password']+'@'+DB['server']+'/'+DB['name'] - # db_result = sql_connect(config.settings.SQLALCHEMY_DB_URI) - log.debug(config.settings.DB) +# Perform final validation of critical infrastructure +validate_critical_config(config.settings) - config.settings.SMTP['server'] = aether_cfg_sql.get('smtp_server') - config.settings.SMTP['port'] = aether_cfg_sql.get('smtp_port') - config.settings.SMTP['username'] = aether_cfg_sql.get('smtp_username') - config.settings.SMTP['password'] = aether_cfg_sql.get('smtp_password') - - # config.settings.FILES_PATH['hosted_files_root'] = aether_cfg_sql.get('PATH_HOSTED_FILES_ROOT') - # config.settings.FILES_PATH['hosted_tmp_root'] = aether_cfg_sql.get('PATH_HOSTED_TMP_ROOT') - - config.settings.FILES_PATH['hosted_files_root'] = aether_cfg_sql.get('path_hosted_files_root') - config.settings.FILES_PATH['hosted_tmp_root'] = aether_cfg_sql.get('path_hosted_tmp_root') -else: - # aether_cfg_sql_result - pass -print('### **** *** ** * Aether API v4 using FastAPI - Finished first SQL SELECT (sql_select()) while loading... * ** *** **** ###' ) -log.debug(aether_cfg_sql_result) +print('### **** *** ** * Aether API v4 using FastAPI - Finished Configuration Phase * ** *** **** ###' ) log.debug(config.settings) # @lru_cache()