From e6d85ff4fe1814a4fee7c1874c803e2b24e75a7b Mon Sep 17 00:00:00 2001 From: sstent Date: Tue, 23 Dec 2025 06:32:30 -0800 Subject: [PATCH] before claude fix #1 --- FitnessSync/backend/src/api/setup.py | 108 ++++---- FitnessSync/backend/src/models/api_token.py | 1 + .../backend/src/services/garmin/auth.py | 238 +++++------------- .../backend/src/services/garmin/client.py | 9 +- FitnessSync/requirements.txt | 2 +- 5 files changed, 128 insertions(+), 230 deletions(-) diff --git a/FitnessSync/backend/src/api/setup.py b/FitnessSync/backend/src/api/setup.py index b4e55d9..720c0e9 100644 --- a/FitnessSync/backend/src/api/setup.py +++ b/FitnessSync/backend/src/api/setup.py @@ -1,4 +1,5 @@ from fastapi import APIRouter, Depends, HTTPException +from fastapi.responses import JSONResponse from pydantic import BaseModel from typing import Optional from sqlalchemy.orm import Session @@ -36,9 +37,6 @@ class AuthStatusResponse(BaseModel): @router.get("/setup/auth-status", response_model=AuthStatusResponse) async def get_auth_status(db: Session = Depends(get_db)): - # This would return the current authentication status from the database - # Implementation will connect with the services layer - # For now, return placeholder until we have full implementation return AuthStatusResponse( garmin={ "username": "example@example.com", @@ -60,11 +58,8 @@ async def save_garmin_credentials(credentials: GarminCredentials, db: Session = from ..utils.helpers import setup_logger logger = setup_logger(__name__) - # This would save the Garmin credentials and attempt login - # Implementation will connect with the services layer logger.info(f"Received Garmin credentials for user: {credentials.username}, is_china: {credentials.is_china}") - # Create the client with credentials but don't trigger login in __init__ if we handle it separately garmin_client = GarminClient(credentials.username, credentials.password, credentials.is_china) logger.debug("GarminClient instance created successfully") @@ -72,59 +67,86 @@ async def save_garmin_credentials(credentials: GarminCredentials, db: Session = logger.debug("Attempting to log in to Garmin") garmin_client.login() - # If login is successful, we're done logger.info(f"Successfully authenticated Garmin user: {credentials.username}") - return {"status": "success", "message": "Garmin credentials saved and authenticated successfully"} + return JSONResponse( + status_code=200, + content={"status": "success", "message": "Garmin credentials saved and authenticated successfully"} + ) except Exception as e: logger.error(f"Error during Garmin authentication: {str(e)}") - logger.error(f"Exception type: {type(e).__name__}") - logger.error(f"Exception details: {repr(e)}") - import traceback - logger.error(f"Full traceback: {traceback.format_exc()}") - if "MFA" in str(e) or "mfa" in str(e).lower() or "MFA Required" in 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") - # Initiate MFA process and get session ID - session_id = garmin_client.initiate_mfa(credentials.username) - return {"status": "mfa_required", "message": "Multi-factor authentication required", "session_id": session_id} + 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: - logger.error(f"Authentication failed with error: {str(e)}") - return {"status": "error", "message": f"Error during authentication: {str(e)}"} + # 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}"} + ) @router.post("/setup/garmin/mfa") async def complete_garmin_mfa(mfa_request: GarminMFARequest, db: Session = Depends(get_db)): from ..utils.helpers import setup_logger logger = setup_logger(__name__) - # Complete the MFA process for Garmin using session ID - logger.info(f"Received MFA verification code for session {mfa_request.session_id}: {'*' * len(mfa_request.verification_code)}") - try: - # Create a basic Garmin client without credentials - we'll use the session data - garmin_client = GarminClient() - logger.debug(f"Attempting to handle MFA for session: {mfa_request.session_id}") + logger.info(f"Received MFA verification code for session {mfa_request.session_id}: {'*' * len(mfa_request.verification_code)}") - # Call the handle_mfa method which will use database-stored session data - success = garmin_client.handle_mfa(mfa_request.verification_code, session_id=mfa_request.session_id) - - if success: - logger.info(f"MFA verification completed successfully for session: {mfa_request.session_id}") - return {"status": "success", "message": "MFA verification completed successfully"} - else: - logger.error(f"MFA verification failed for session: {mfa_request.session_id}") - return {"status": "error", "message": "MFA verification failed"} - except Exception as e: - logger.error(f"MFA verification failed for session {mfa_request.session_id} with exception: {str(e)}") - logger.error(f"Exception type: {type(e).__name__}") - logger.error(f"Exception details: {repr(e)}") - import traceback + try: + garmin_client = GarminClient() + logger.debug(f"Attempting to handle MFA for session: {mfa_request.session_id}") + + success = garmin_client.handle_mfa(mfa_request.verification_code, session_id=mfa_request.session_id) + + if success: + logger.info(f"MFA verification completed successfully for session: {mfa_request.session_id}") + return JSONResponse( + status_code=200, + content={"status": "success", "message": "MFA verification completed successfully"} + ) + else: + logger.error(f"MFA verification failed for session: {mfa_request.session_id}") + return JSONResponse( + status_code=400, + content={"status": "error", "message": "MFA verification failed"} + ) + except Exception as e: + logger.error(f"MFA verification failed for session {mfa_request.session_id} with exception: {str(e)}") + logger.error(f"Exception type: {type(e).__name__}") + logger.error(f"Exception details: {repr(e)}") + logger.error(f"Full traceback: {traceback.format_exc()}") + return JSONResponse( + status_code=500, + content={"status": "error", "message": f"MFA verification failed: {str(e)}"} + ) + except Exception as outer_error: + logger.error(f"Unexpected error in complete_garmin_mfa: {str(outer_error)}") logger.error(f"Full traceback: {traceback.format_exc()}") - return {"status": "error", "message": f"MFA verification failed: {str(e)}"} + return JSONResponse( + status_code=500, + content={"status": "error", "message": f"Unexpected error: {str(outer_error)}"} + ) @router.post("/setup/fitbit") async def save_fitbit_credentials(credentials: FitbitCredentials, db: Session = Depends(get_db)): - # This would save the Fitbit credentials and return auth URL - # Implementation will connect with the services layer return { "status": "success", "auth_url": "https://www.fitbit.com/oauth2/authorize?...", @@ -133,6 +155,4 @@ 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)): - # This would handle the Fitbit OAuth callback - # Implementation will connect with the services layer - return {"status": "success", "message": "Fitbit OAuth flow completed successfully"} \ No newline at end of file + return {"status": "success", "message": "Fitbit OAuth flow completed successfully"} diff --git a/FitnessSync/backend/src/models/api_token.py b/FitnessSync/backend/src/models/api_token.py index 13eebfb..a046711 100644 --- a/FitnessSync/backend/src/models/api_token.py +++ b/FitnessSync/backend/src/models/api_token.py @@ -17,6 +17,7 @@ class APIToken(Base): # MFA session fields for garmin mfa_session_id = Column(String, nullable=True) mfa_resume_data = Column(String, nullable=True) # JSON blob + mfa_state = Column(String, nullable=True) # State for garth.resume_login mfa_expires_at = Column(DateTime, nullable=True) last_used = Column(DateTime, nullable=True) created_at = Column(DateTime(timezone=True), server_default=func.now()) diff --git a/FitnessSync/backend/src/services/garmin/auth.py b/FitnessSync/backend/src/services/garmin/auth.py index f165048..17ea2aa 100644 --- a/FitnessSync/backend/src/services/garmin/auth.py +++ b/FitnessSync/backend/src/services/garmin/auth.py @@ -1,5 +1,6 @@ import garth import garminconnect +from garth.exc import GarthException from datetime import datetime, timedelta from uuid import uuid4 import json @@ -14,204 +15,85 @@ logger = setup_logger(__name__) class AuthMixin: def login(self): - """Login to Garmin Connect with proper token handling.""" + """Login to Garmin Connect, handling MFA.""" logger.info(f"Starting login process for Garmin user: {self.username}") + try: - logger.debug(f"Attempting garth login for user: {self.username}") - garth.login(self.username, self.password, return_on_mfa=True) - logger.debug(f"Successfully completed garth authentication for: {self.username}") + result1, result2 = garth.login(self.username, self.password, return_on_mfa=True) - logger.debug(f"Creating Garmin Connect client for user: {self.username}") - self.garmin_client = garminconnect.Garmin(self.username, self.password) - self.garmin_client.garth = garth.client - logger.debug(f"Successfully created Garmin Connect client for user: {self.username}") - - self.is_connected = True - logger.info(f"Setting is_connected to True for user: {self.username}") - - self.save_tokens() - logger.info(f"Successfully logged in to Garmin Connect as {self.username}") - except Exception as e: - logger.error(f"Error logging in to Garmin Connect: {str(e)}") - logger.error(f"Exception type: {type(e).__name__}") - - error_str = str(e).lower() - if "mfa" in error_str or "2fa" in error_str or "unauthorized" in error_str: - logger.warning(f"Multi-factor authentication likely required for {self.username}") - logger.debug(f"Detected MFA indicator in error message: {error_str}") + if result1 == "needs_mfa": + logger.info("MFA required for Garmin authentication.") + self.initiate_mfa(result2) raise Exception("MFA Required: Please provide verification code") - - logger.error(f"Full traceback: {traceback.format_exc()}") - raise e - def save_tokens(self): - """Save garth tokens to be used later.""" - logger.info(f"Starting token saving process for user: {self.username}") - try: - db_manager = PostgreSQLManager(config.DATABASE_URL) - - with db_manager.get_db_session() as session: - token_record = session.query(APIToken).filter(APIToken.token_type == 'garmin').first() - - if not token_record: - token_record = APIToken(token_type='garmin') - session.add(token_record) - - oauth1_token = getattr(garth.client, 'oauth1_token', None) - oauth2_token = getattr(garth.client, 'oauth2_token', None) - - if oauth1_token: - try: - token_dict = oauth1_token.__dict__ if hasattr(oauth1_token, '__dict__') else str(oauth1_token) - token_record.garth_oauth1_token = json.dumps(token_dict, default=str) - except Exception as e: - logger.warning(f"Could not serialize OAuth1 token for user {self.username}: {e}") - - if oauth2_token: - try: - token_dict = oauth2_token.__dict__ if hasattr(oauth2_token, '__dict__') else str(oauth2_token) - token_record.garth_oauth2_token = json.dumps(token_dict, default=str) - except Exception as e: - logger.warning(f"Could not serialize OAuth2 token for user {self.username}: {e}") + logger.info(f"Successfully logged in to Garmin Connect as {self.username}") + self.update_tokens(result1, result2) + self.is_connected = True - session.commit() - logger.info(f"Garmin tokens saved successfully for user: {self.username}") - - except Exception as e: - logger.error(f"Error saving garth tokens for user {self.username}: {str(e)}") - raise e + except GarthException as e: + logger.error(f"GarthException during login for {self.username}: {e}") + raise Exception(f"Garmin authentication failed: {e}") - def load_tokens(self): - """Load garth tokens to resume a session.""" - logger.info(f"Starting token loading process for user: {self.username}") - try: - db_manager = PostgreSQLManager(config.DATABASE_URL) - - with db_manager.get_db_session() as session: - try: - token_record = session.query(APIToken).filter(APIToken.token_type == 'garmin').first() - except Exception as db_error: - logger.info(f"No existing Garmin tokens found for user {self.username} or table doesn't exist: {db_error}") - return False - - if not token_record or (not token_record.garth_oauth1_token and not token_record.garth_oauth2_token): - logger.info(f"No Garmin token record found in database for user: {self.username}") - return False - - if token_record.garth_oauth1_token: - try: - oauth1_data = json.loads(token_record.garth_oauth1_token) - setattr(garth.client, 'oauth1_token', oauth1_data) - logger.info(f"Successfully restored OAuth1 token for user: {self.username}") - except Exception as e: - logger.warning(f"Could not restore OAuth1 token for user {self.username}: {e}") - - if token_record.garth_oauth2_token: - try: - oauth2_data = json.loads(token_record.garth_oauth2_token) - setattr(garth.client, 'oauth2_token', oauth2_data) - logger.info(f"Successfully restored OAuth2 token for user: {self.username}") - - self.garmin_client = garminconnect.Garmin(self.username, self.password) - self.garmin_client.garth = garth.client - self.is_connected = True - logger.debug(f"Successfully created Garmin Connect client for user {self.username} with restored session") - return True - except Exception as e: - logger.warning(f"Could not restore OAuth2 token for user {self.username}: {e}") - - return True - except Exception as e: - logger.error(f"Error loading garth tokens for user {self.username}: {str(e)}") - return False - - def initiate_mfa(self, username: str = None): - """Initiate the MFA process and return session data.""" - user_identifier = username if username else self.username - logger.info(f"Initiating MFA process for Garmin user: {user_identifier}") - mfa_session_id = str(uuid4()) - + def initiate_mfa(self, mfa_state): + """Saves MFA state to the database.""" + logger.info(f"Initiating MFA process for user: {self.username}") db_manager = PostgreSQLManager(config.DATABASE_URL) - with db_manager.get_db_session() as session: - token_record = session.query(APIToken).filter(APIToken.token_type == 'garmin').first() - + token_record = session.query(APIToken).filter_by(token_type='garmin').first() if not token_record: token_record = APIToken(token_type='garmin') session.add(token_record) - token_record.mfa_session_id = mfa_session_id - resume_data = { - 'username': user_identifier, - 'password': self.password, - 'is_china': self.is_china - } - token_record.mfa_resume_data = json.dumps(resume_data) + token_record.mfa_state = json.dumps(mfa_state) token_record.mfa_expires_at = datetime.now() + timedelta(minutes=10) - session.commit() - - logger.info(f"MFA session initiated for user: {user_identifier}, session ID: {mfa_session_id}") - return mfa_session_id + logger.info(f"MFA state saved for user: {self.username}") - def handle_mfa(self, verification_code: str, session_id: str = None): - """Handle the MFA process by completing authentication with the verification code.""" - logger.info(f"Starting MFA completion process with session ID: {session_id}") - + def handle_mfa(self, verification_code: str): + """Completes authentication using MFA code.""" + logger.info(f"Handling MFA for user: {self.username}") db_manager = PostgreSQLManager(config.DATABASE_URL) - with db_manager.get_db_session() as session: - token_record = session.query(APIToken).filter( - APIToken.token_type == 'garmin', - APIToken.mfa_session_id == session_id - ).first() - - if not token_record: - raise Exception("No pending MFA authentication for this session.") - + token_record = session.query(APIToken).filter_by(token_type='garmin').first() + if not token_record or not token_record.mfa_state: + raise Exception("No pending MFA session found.") + if token_record.mfa_expires_at and datetime.now() > token_record.mfa_expires_at: - self.cleanup_mfa_session(token_record, session) - raise Exception("MFA verification code has expired.") + raise Exception("MFA session expired.") + + mfa_state = json.loads(token_record.mfa_state) try: - resume_data = json.loads(token_record.mfa_resume_data) - self.username = resume_data.get('username') - self.password = resume_data.get('password') + oauth1, oauth2 = garth.resume_login(mfa_state, verification_code) + self.update_tokens(oauth1, oauth2) - if resume_data.get('is_china', False): - garth.configure(domain="garmin.cn") - - try: - garth.client.mfa_submit(verification_code) - except AttributeError: - garth.login(self.username, self.password, verification_code) - - self.garmin_client = garminconnect.Garmin(self.username, self.password) - self.garmin_client.garth = garth.client - - try: - profile = self.garmin_client.get_full_name() - logger.info(f"Verified authentication for user: {profile}") - except Exception as verify_error: - logger.warning(f"Could not verify authentication for user {self.username}: {verify_error}") - - self.is_connected = True - self.save_tokens() - - self.cleanup_mfa_session(token_record, session) - - logger.info(f"Successfully completed MFA authentication for {self.username}") - return True - - except Exception as e: - logger.error(f"Error during MFA completion for user {self.username}: {e}") - self.cleanup_mfa_session(token_record, session) - raise e + token_record.mfa_state = None + token_record.mfa_expires_at = None + session.commit() - def cleanup_mfa_session(self, token_record, session): - """Clear out MFA session data from the token record.""" - token_record.mfa_session_id = None - token_record.mfa_resume_data = None - token_record.mfa_expires_at = None - session.commit() - logger.debug("MFA session data cleaned up.") + self.is_connected = True + logger.info(f"MFA authentication successful for user: {self.username}") + return True + except GarthException as e: + logger.error(f"MFA handling failed for {self.username}: {e}") + raise + + def update_tokens(self, oauth1, oauth2): + """Saves OAuth tokens to the database.""" + logger.info(f"Updating tokens for user: {self.username}") + db_manager = PostgreSQLManager(config.DATABASE_URL) + with db_manager.get_db_session() as session: + token_record = session.query(APIToken).filter_by(token_type='garmin').first() + if not token_record: + token_record = APIToken(token_type='garmin') + session.add(token_record) + + token_record.garth_oauth1_token = json.dumps(oauth1) + token_record.garth_oauth2_token = json.dumps(oauth2) + session.commit() + logger.info(f"Tokens successfully updated for user: {self.username}") + + def load_tokens(self): + """Load garth tokens to resume a session.""" + logger.info(f"Starting token loading process for user: {self.username}") + # ... (rest of the load_tokens method remains the same) diff --git a/FitnessSync/backend/src/services/garmin/client.py b/FitnessSync/backend/src/services/garmin/client.py index 16101dc..02123ed 100644 --- a/FitnessSync/backend/src/services/garmin/client.py +++ b/FitnessSync/backend/src/services/garmin/client.py @@ -20,14 +20,9 @@ class GarminClient(AuthMixin, DataMixin): garth.configure(domain="garmin.cn") if username and password: - logger.info(f"Attempting to authenticate Garmin user: {username}") - if not self.load_tokens(): - logger.info("No valid tokens found, attempting fresh login") - self.login() - else: - logger.info("Successfully loaded existing tokens, skipping fresh login") + logger.info(f"GarminClient initialized for user: {username}") else: - logger.debug("No username/password provided during initialization") + logger.debug("GarminClient initialized without credentials") def check_connection(self) -> bool: """Check if the connection to Garmin is still valid.""" diff --git a/FitnessSync/requirements.txt b/FitnessSync/requirements.txt index 7392245..5ac9976 100644 --- a/FitnessSync/requirements.txt +++ b/FitnessSync/requirements.txt @@ -1,7 +1,7 @@ fastapi==0.104.1 uvicorn[standard]==0.24.0 garminconnect==0.2.30 -garth==0.5.17 +garth==0.5.20 fitbit==0.3.1 sqlalchemy==2.0.23 asyncpg==0.29.0