Files
FitTrack2/FitnessSync/backend/src/api/config_routes.py
sstent d1cfd0fd8e feat: implement Fitbit OAuth, Garmin MFA, and optimize segment discovery
- Add Fitbit authentication flow (save credentials, OAuth callback handling)
- Implement Garmin MFA support with successful session/cookie handling
- Optimize segment discovery with new sampling and activity query services
- Refactor database session management in discovery API for better testability
- Enhance activity data parsing for charts and analysis
- Update tests to use testcontainers and proper dependency injection
- Clean up repository by ignoring and removing tracked transient files (.pyc, .db)
2026-01-16 15:35:26 -08:00

118 lines
4.7 KiB
Python

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)}")