from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session import logging import requests import base64 import json from ..services.postgresql_manager import PostgreSQLManager from ..utils.config import config from .status import get_db router = APIRouter() logger = logging.getLogger(__name__) @router.post("/setup/load-consul-config") def load_consul_config(db: Session = Depends(get_db)): logger = logging.getLogger(__name__) logger.info("Attempting to load configuration from Consul...") try: # User defined Consul URL consul_host = "consul.service.dc1.consul" consul_port = "8500" app_prefix = "fitbit-garmin-sync/" consul_url = f"http://{consul_host}:{consul_port}/v1/kv/{app_prefix}?recurse=true" logger.debug(f"Connecting to Consul at: {consul_url}") response = requests.get(consul_url, timeout=5) if response.status_code == 404: logger.warning(f"No configuration found in Consul under '{app_prefix}'") raise HTTPException(status_code=404, detail="No configuration found in Consul") response.raise_for_status() data = response.json() config_map = {} # Helper to decode Consul values def decode_consul_value(val): if not val: return None try: return base64.b64decode(val).decode('utf-8') except Exception as e: logger.warning(f"Failed to decode value: {e}") return None # Pass 1: Load all raw keys for item in data: key = item['Key'].replace(app_prefix, '') value = decode_consul_value(item.get('Value')) if value: config_map[key] = value # Pass 2: Check for special 'config' key (JSON blob) if 'config' in config_map: try: json_config = json.loads(config_map['config']) logger.debug("Found 'config' key with JSON content, merging...") config_map.update(json_config) except json.JSONDecodeError: logger.warning("'config' key found but is not valid JSON, ignoring as blob.") logger.debug(f"Resolved configuration keys: {list(config_map.keys())}") # Look for standard keys username = config_map.get('garmin_username') or config_map.get('USERNAME') password = config_map.get('garmin_password') or config_map.get('PASSWORD') is_china = str(config_map.get('is_china', 'false')).lower() == 'true' if not username and isinstance(config_map.get('garmin'), dict): logger.debug("Found nested 'garmin' config object.") garmin_conf = config_map['garmin'] username = garmin_conf.get('username') password = garmin_conf.get('password') if 'is_china' in garmin_conf: is_china = str(garmin_conf.get('is_china')).lower() == 'true' if not username or not password: logger.error("Consul config resolved but missing 'garmin_username' or 'garmin_password'") raise HTTPException(status_code=400, detail="Consul config missing credentials") # Extract Fitbit credentials fitbit_client_id = config_map.get('fitbit_client_id') fitbit_client_secret = config_map.get('fitbit_client_secret') fitbit_redirect_uri = config_map.get('fitbit_redirect_uri') if isinstance(config_map.get('fitbit'), dict): logger.debug("Found nested 'fitbit' config object.") fitbit_conf = config_map['fitbit'] fitbit_client_id = fitbit_conf.get('client_id') fitbit_client_secret = fitbit_conf.get('client_secret') logger.info("Consul config loaded successfully. Returning to frontend.") return { "status": "success", "message": "Configuration loaded from Consul", "garmin": { "username": username, "password": password, "is_china": is_china }, "fitbit": { "client_id": fitbit_client_id, "client_secret": fitbit_client_secret, "redirect_uri": fitbit_redirect_uri } } except requests.exceptions.RequestException as e: logger.error(f"Failed to connect to Consul: {e}") raise HTTPException(status_code=502, detail=f"Failed to connect to Consul: {str(e)}") except HTTPException: raise except Exception as e: logger.error(f"Error loading from Consul: {e}", exc_info=True) raise HTTPException(status_code=500, detail=f"Internal error loading config: {str(e)}")