before claude fix #1

This commit is contained in:
2025-12-23 06:32:30 -08:00
parent a23fa1b30d
commit e6d85ff4fe
5 changed files with 128 additions and 230 deletions

View File

@@ -1,4 +1,5 @@
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import JSONResponse
from pydantic import BaseModel from pydantic import BaseModel
from typing import Optional from typing import Optional
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
@@ -36,9 +37,6 @@ class AuthStatusResponse(BaseModel):
@router.get("/setup/auth-status", response_model=AuthStatusResponse) @router.get("/setup/auth-status", response_model=AuthStatusResponse)
async def get_auth_status(db: Session = Depends(get_db)): 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( return AuthStatusResponse(
garmin={ garmin={
"username": "example@example.com", "username": "example@example.com",
@@ -60,11 +58,8 @@ async def save_garmin_credentials(credentials: GarminCredentials, db: Session =
from ..utils.helpers import setup_logger from ..utils.helpers import setup_logger
logger = setup_logger(__name__) 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}") 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) garmin_client = GarminClient(credentials.username, credentials.password, credentials.is_china)
logger.debug("GarminClient instance created successfully") 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") logger.debug("Attempting to log in to Garmin")
garmin_client.login() garmin_client.login()
# If login is successful, we're done
logger.info(f"Successfully authenticated Garmin user: {credentials.username}") 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: except Exception as e:
logger.error(f"Error during Garmin authentication: {str(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") logger.info("MFA required for Garmin authentication")
# Initiate MFA process and get session ID try:
session_id = garmin_client.initiate_mfa(credentials.username) session_id = garmin_client.initiate_mfa(credentials.username)
return {"status": "mfa_required", "message": "Multi-factor authentication required", "session_id": session_id} 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: else:
logger.error(f"Authentication failed with error: {str(e)}") # For other exceptions during login, return a generic error
return {"status": "error", "message": f"Error during authentication: {str(e)}"} return JSONResponse(
status_code=500,
content={"status": "error", "message": f"An unexpected error occurred: {error_message}"}
)
@router.post("/setup/garmin/mfa") @router.post("/setup/garmin/mfa")
async def complete_garmin_mfa(mfa_request: GarminMFARequest, db: Session = Depends(get_db)): async def complete_garmin_mfa(mfa_request: GarminMFARequest, db: Session = Depends(get_db)):
from ..utils.helpers import setup_logger from ..utils.helpers import setup_logger
logger = setup_logger(__name__) 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: try:
# Create a basic Garmin client without credentials - we'll use the session data logger.info(f"Received MFA verification code for session {mfa_request.session_id}: {'*' * len(mfa_request.verification_code)}")
garmin_client = GarminClient()
logger.debug(f"Attempting to handle MFA for session: {mfa_request.session_id}")
# Call the handle_mfa method which will use database-stored session data try:
success = garmin_client.handle_mfa(mfa_request.verification_code, session_id=mfa_request.session_id) garmin_client = GarminClient()
logger.debug(f"Attempting to handle MFA for session: {mfa_request.session_id}")
if success:
logger.info(f"MFA verification completed successfully for session: {mfa_request.session_id}") success = garmin_client.handle_mfa(mfa_request.verification_code, session_id=mfa_request.session_id)
return {"status": "success", "message": "MFA verification completed successfully"}
else: if success:
logger.error(f"MFA verification failed for session: {mfa_request.session_id}") logger.info(f"MFA verification completed successfully for session: {mfa_request.session_id}")
return {"status": "error", "message": "MFA verification failed"} return JSONResponse(
except Exception as e: status_code=200,
logger.error(f"MFA verification failed for session {mfa_request.session_id} with exception: {str(e)}") content={"status": "success", "message": "MFA verification completed successfully"}
logger.error(f"Exception type: {type(e).__name__}") )
logger.error(f"Exception details: {repr(e)}") else:
import traceback 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()}") 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") @router.post("/setup/fitbit")
async def save_fitbit_credentials(credentials: FitbitCredentials, db: Session = Depends(get_db)): 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 { return {
"status": "success", "status": "success",
"auth_url": "https://www.fitbit.com/oauth2/authorize?...", "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") @router.post("/setup/fitbit/callback")
async def fitbit_callback(callback_data: FitbitCallback, db: Session = Depends(get_db)): async def fitbit_callback(callback_data: FitbitCallback, db: Session = Depends(get_db)):
# This would handle the Fitbit OAuth callback return {"status": "success", "message": "Fitbit OAuth flow completed successfully"}
# Implementation will connect with the services layer
return {"status": "success", "message": "Fitbit OAuth flow completed successfully"}

View File

@@ -17,6 +17,7 @@ class APIToken(Base):
# MFA session fields for garmin # MFA session fields for garmin
mfa_session_id = Column(String, nullable=True) mfa_session_id = Column(String, nullable=True)
mfa_resume_data = Column(String, nullable=True) # JSON blob 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) mfa_expires_at = Column(DateTime, nullable=True)
last_used = Column(DateTime, nullable=True) last_used = Column(DateTime, nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now()) created_at = Column(DateTime(timezone=True), server_default=func.now())

View File

@@ -1,5 +1,6 @@
import garth import garth
import garminconnect import garminconnect
from garth.exc import GarthException
from datetime import datetime, timedelta from datetime import datetime, timedelta
from uuid import uuid4 from uuid import uuid4
import json import json
@@ -14,204 +15,85 @@ logger = setup_logger(__name__)
class AuthMixin: class AuthMixin:
def login(self): 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}") logger.info(f"Starting login process for Garmin user: {self.username}")
try: try:
logger.debug(f"Attempting garth login for user: {self.username}") result1, result2 = garth.login(self.username, self.password, return_on_mfa=True)
garth.login(self.username, self.password, return_on_mfa=True)
logger.debug(f"Successfully completed garth authentication for: {self.username}")
logger.debug(f"Creating Garmin Connect client for user: {self.username}") if result1 == "needs_mfa":
self.garmin_client = garminconnect.Garmin(self.username, self.password) logger.info("MFA required for Garmin authentication.")
self.garmin_client.garth = garth.client self.initiate_mfa(result2)
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}")
raise Exception("MFA Required: Please provide verification code") raise Exception("MFA Required: Please provide verification code")
logger.error(f"Full traceback: {traceback.format_exc()}")
raise e
def save_tokens(self): logger.info(f"Successfully logged in to Garmin Connect as {self.username}")
"""Save garth tokens to be used later.""" self.update_tokens(result1, result2)
logger.info(f"Starting token saving process for user: {self.username}") self.is_connected = True
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}")
session.commit() except GarthException as e:
logger.info(f"Garmin tokens saved successfully for user: {self.username}") logger.error(f"GarthException during login for {self.username}: {e}")
raise Exception(f"Garmin authentication failed: {e}")
except Exception as e:
logger.error(f"Error saving garth tokens for user {self.username}: {str(e)}")
raise e
def load_tokens(self): def initiate_mfa(self, mfa_state):
"""Load garth tokens to resume a session.""" """Saves MFA state to the database."""
logger.info(f"Starting token loading process for user: {self.username}") logger.info(f"Initiating MFA 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())
db_manager = PostgreSQLManager(config.DATABASE_URL) db_manager = PostgreSQLManager(config.DATABASE_URL)
with db_manager.get_db_session() as session: 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: if not token_record:
token_record = APIToken(token_type='garmin') token_record = APIToken(token_type='garmin')
session.add(token_record) session.add(token_record)
token_record.mfa_session_id = mfa_session_id token_record.mfa_state = json.dumps(mfa_state)
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_expires_at = datetime.now() + timedelta(minutes=10) token_record.mfa_expires_at = datetime.now() + timedelta(minutes=10)
session.commit() session.commit()
logger.info(f"MFA state saved for user: {self.username}")
logger.info(f"MFA session initiated for user: {user_identifier}, session ID: {mfa_session_id}")
return mfa_session_id
def handle_mfa(self, verification_code: str, session_id: str = None): def handle_mfa(self, verification_code: str):
"""Handle the MFA process by completing authentication with the verification code.""" """Completes authentication using MFA code."""
logger.info(f"Starting MFA completion process with session ID: {session_id}") logger.info(f"Handling MFA for user: {self.username}")
db_manager = PostgreSQLManager(config.DATABASE_URL) db_manager = PostgreSQLManager(config.DATABASE_URL)
with db_manager.get_db_session() as session: with db_manager.get_db_session() as session:
token_record = session.query(APIToken).filter( token_record = session.query(APIToken).filter_by(token_type='garmin').first()
APIToken.token_type == 'garmin', if not token_record or not token_record.mfa_state:
APIToken.mfa_session_id == session_id raise Exception("No pending MFA session found.")
).first()
if not token_record:
raise Exception("No pending MFA authentication for this session.")
if token_record.mfa_expires_at and datetime.now() > token_record.mfa_expires_at: if token_record.mfa_expires_at and datetime.now() > token_record.mfa_expires_at:
self.cleanup_mfa_session(token_record, session) raise Exception("MFA session expired.")
raise Exception("MFA verification code has expired.")
mfa_state = json.loads(token_record.mfa_state)
try: try:
resume_data = json.loads(token_record.mfa_resume_data) oauth1, oauth2 = garth.resume_login(mfa_state, verification_code)
self.username = resume_data.get('username') self.update_tokens(oauth1, oauth2)
self.password = resume_data.get('password')
if resume_data.get('is_china', False): token_record.mfa_state = None
garth.configure(domain="garmin.cn") token_record.mfa_expires_at = None
session.commit()
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
def cleanup_mfa_session(self, token_record, session): self.is_connected = True
"""Clear out MFA session data from the token record.""" logger.info(f"MFA authentication successful for user: {self.username}")
token_record.mfa_session_id = None return True
token_record.mfa_resume_data = None except GarthException as e:
token_record.mfa_expires_at = None logger.error(f"MFA handling failed for {self.username}: {e}")
session.commit() raise
logger.debug("MFA session data cleaned up.")
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)

View File

@@ -20,14 +20,9 @@ class GarminClient(AuthMixin, DataMixin):
garth.configure(domain="garmin.cn") garth.configure(domain="garmin.cn")
if username and password: if username and password:
logger.info(f"Attempting to authenticate Garmin user: {username}") logger.info(f"GarminClient initialized for 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")
else: else:
logger.debug("No username/password provided during initialization") logger.debug("GarminClient initialized without credentials")
def check_connection(self) -> bool: def check_connection(self) -> bool:
"""Check if the connection to Garmin is still valid.""" """Check if the connection to Garmin is still valid."""

View File

@@ -1,7 +1,7 @@
fastapi==0.104.1 fastapi==0.104.1
uvicorn[standard]==0.24.0 uvicorn[standard]==0.24.0
garminconnect==0.2.30 garminconnect==0.2.30
garth==0.5.17 garth==0.5.20
fitbit==0.3.1 fitbit==0.3.1
sqlalchemy==2.0.23 sqlalchemy==2.0.23
asyncpg==0.29.0 asyncpg==0.29.0