This commit is contained in:
2025-12-24 18:12:11 -08:00
parent 4e156242eb
commit 8fe375a966
22 changed files with 5397 additions and 209 deletions

View File

@@ -4,8 +4,12 @@ from pydantic import BaseModel
from typing import Optional
from sqlalchemy.orm import Session
import traceback
import httpx
import base64
import json
from ..services.postgresql_manager import PostgreSQLManager
from ..utils.config import config
import garth
from ..services.garmin.client import GarminClient
router = APIRouter()
@@ -35,22 +39,117 @@ class AuthStatusResponse(BaseModel):
garmin: Optional[dict] = None
fitbit: Optional[dict] = None
class AuthStatusResponse(BaseModel):
garmin: Optional[dict] = None
fitbit: Optional[dict] = None
@router.post("/setup/load-consul-config")
async def load_consul_config(db: Session = Depends(get_db)):
"""
Load configuration from Consul and save it to the database.
It first tries to use tokens from Consul, if they are not present, it falls back to username/password login.
"""
consul_url = "http://consul.service.dc1.consul:8500/v1/kv/fitbit-garmin-sync/config"
try:
async with httpx.AsyncClient() as client:
response = await client.get(consul_url)
response.raise_for_status()
data = response.json()
if not (data and 'Value' in data[0]):
raise HTTPException(status_code=404, detail="Config not found in Consul")
config_value = base64.b64decode(data[0]['Value']).decode('utf-8')
config = json.loads(config_value)
if 'garmin' in config:
garmin_config = config['garmin']
from ..models.api_token import APIToken
from datetime import datetime
# Prefer tokens if available
if 'garth_oauth1_token' in garmin_config and 'garth_oauth2_token' in garmin_config:
token_record = db.query(APIToken).filter_by(token_type='garmin').first()
if not token_record:
token_record = APIToken(token_type='garmin')
db.add(token_record)
token_record.garth_oauth1_token = garmin_config['garth_oauth1_token']
token_record.garth_oauth2_token = garmin_config['garth_oauth2_token']
token_record.updated_at = datetime.now()
db.commit()
return {"status": "success", "message": "Garmin tokens from Consul have been saved."}
# Fallback to username/password login
elif 'username' in garmin_config and 'password' in garmin_config:
garmin_creds = GarminCredentials(**garmin_config)
garmin_client = GarminClient(garmin_creds.username, garmin_creds.password, garmin_creds.is_china)
status = garmin_client.login()
if status == "mfa_required":
return {"status": "mfa_required", "message": "Garmin login from Consul requires MFA. Please complete it manually."}
elif status != "success":
raise HTTPException(status_code=400, detail=f"Failed to login to Garmin with Consul credentials: {status}")
# TODO: Add Fitbit credentials handling
return {"status": "success", "message": "Configuration from Consul processed."}
except httpx.RequestError as e:
raise HTTPException(status_code=500, detail=f"Failed to connect to Consul: {e}")
except Exception as e:
import traceback
traceback.print_exc()
raise HTTPException(status_code=500, detail=f"An error occurred: {e}")
@router.get("/setup/auth-status", response_model=AuthStatusResponse)
async def get_auth_status(db: Session = Depends(get_db)):
return AuthStatusResponse(
garmin={
"username": "example@example.com",
"authenticated": False,
"token_expires_at": None,
"last_login": None,
"is_china": False
},
fitbit={
"client_id": "example_client_id",
"authenticated": False,
"token_expires_at": None,
"last_login": None
from ..models.api_token import APIToken
garmin_status = {}
fitbit_status = {}
# Garmin Status
garmin_token = db.query(APIToken).filter_by(token_type='garmin').first()
if garmin_token:
garmin_status = {
"token_stored": True,
"authenticated": garmin_token.garth_oauth1_token is not None and garmin_token.garth_oauth2_token is not None,
"garth_oauth1_token_exists": garmin_token.garth_oauth1_token is not None,
"garth_oauth2_token_exists": garmin_token.garth_oauth2_token is not None,
"mfa_state_exists": garmin_token.mfa_state is not None,
"mfa_expires_at": garmin_token.mfa_expires_at,
"last_used": garmin_token.last_used,
"updated_at": garmin_token.updated_at,
"username": "N/A", # Placeholder, username is not stored in APIToken
"is_china": False # Placeholder
}
else:
garmin_status = {
"token_stored": False,
"authenticated": False
}
# Fitbit Status (Existing logic, might need adjustment if Fitbit tokens are stored differently)
fitbit_token = db.query(APIToken).filter_by(token_type='fitbit').first()
if fitbit_token:
fitbit_status = {
"token_stored": True,
"authenticated": fitbit_token.access_token is not None,
"client_id": fitbit_token.access_token[:10] + "..." if fitbit_token.access_token else "N/A",
"expires_at": fitbit_token.expires_at,
"last_used": fitbit_token.last_used,
"updated_at": fitbit_token.updated_at
}
else:
fitbit_status = {
"token_stored": False,
"authenticated": False
}
return AuthStatusResponse(
garmin=garmin_status,
fitbit=fitbit_status
)
@router.post("/setup/garmin")
@@ -63,44 +162,25 @@ async def save_garmin_credentials(credentials: GarminCredentials, db: Session =
garmin_client = GarminClient(credentials.username, credentials.password, credentials.is_china)
logger.debug("GarminClient instance created successfully")
try:
logger.debug("Attempting to log in to Garmin")
garmin_client.login()
logger.info(f"Successfully authenticated Garmin user: {credentials.username}")
logger.debug("Attempting to log in to Garmin")
# Check the status returned directly
status = garmin_client.login()
if status == "mfa_required":
# Hardcode the session_id as 'garmin' since you use a single record in APIToken
return JSONResponse(
status_code=200,
content={"status": "success", "message": "Garmin credentials saved and authenticated successfully"}
content={
"status": "mfa_required",
"message": "MFA Required",
"session_id": "garmin"
}
)
except Exception as e:
logger.error(f"Error during Garmin authentication: {str(e)}")
error_message = str(e)
if "MFA" in error_message or "mfa" in error_message.lower() or "MFA Required" in error_message:
logger.info("MFA required for Garmin authentication")
try:
session_id = garmin_client.initiate_mfa(credentials.username)
return JSONResponse(
status_code=200,
content={
"status": "mfa_required",
"message": "Multi-factor authentication required",
"session_id": session_id
}
)
except Exception as mfa_error:
logger.error(f"Error initiating MFA: {str(mfa_error)}")
return JSONResponse(
status_code=500,
content={"status": "error", "message": f"Error initiating MFA: {str(mfa_error)}"}
)
else:
# For other exceptions during login, return a generic error
return JSONResponse(
status_code=500,
content={"status": "error", "message": f"An unexpected error occurred: {error_message}"}
)
return JSONResponse(
status_code=200,
content={"status": "success", "message": "Logged in!"}
)
@router.post("/setup/garmin/mfa")
async def complete_garmin_mfa(mfa_request: GarminMFARequest, db: Session = Depends(get_db)):
@@ -156,3 +236,48 @@ async def save_fitbit_credentials(credentials: FitbitCredentials, db: Session =
@router.post("/setup/fitbit/callback")
async def fitbit_callback(callback_data: FitbitCallback, db: Session = Depends(get_db)):
return {"status": "success", "message": "Fitbit OAuth flow completed successfully"}
@router.post("/setup/garmin/test-token")
async def test_garmin_token(db: Session = Depends(get_db)):
from ..models.api_token import APIToken
from garth.auth_tokens import OAuth1Token, OAuth2Token
import json
token_record = db.query(APIToken).filter_by(token_type='garmin').first()
if not token_record or not token_record.garth_oauth1_token or not token_record.garth_oauth2_token:
raise HTTPException(status_code=404, detail="Garmin token not found or incomplete.")
try:
from ..utils.helpers import setup_logger
logger = setup_logger(__name__)
logger.info("garth_oauth1_token from DB: %s", token_record.garth_oauth1_token)
logger.info("Type of garth_oauth1_token: %s", type(token_record.garth_oauth1_token))
logger.info("garth_oauth2_token from DB: %s", token_record.garth_oauth2_token)
logger.info("Type of garth_oauth2_token: %s", type(token_record.garth_oauth2_token))
if not token_record.garth_oauth1_token or not token_record.garth_oauth2_token:
raise HTTPException(status_code=400, detail="OAuth1 or OAuth2 token is empty.")
import garth
# Parse JSON to dictionaries
oauth1_dict = json.loads(token_record.garth_oauth1_token)
oauth2_dict = json.loads(token_record.garth_oauth2_token)
# Convert to proper token objects
garth.client.oauth1_token = OAuth1Token(**oauth1_dict)
garth.client.oauth2_token = OAuth2Token(**oauth2_dict)
# Also configure the domain if present
if oauth1_dict.get('domain'):
garth.configure(domain=oauth1_dict['domain'])
profile_info = garth.UserProfile.get()
return profile_info
except Exception as e:
import traceback
traceback.print_exc()
raise HTTPException(status_code=500, detail=f"Failed to test Garmin token: {e}")