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

@@ -1,86 +1,38 @@
import garth
import garminconnect
from garth.exc import GarthException
from datetime import datetime, timedelta
from uuid import uuid4
import json
import traceback
from src.utils.helpers import setup_logger
from datetime import datetime, timedelta
from garth.exc import GarthException
from src.models.api_token import APIToken
from src.services.postgresql_manager import PostgreSQLManager
from src.utils.config import config
from src.utils.helpers import setup_logger
logger = setup_logger(__name__)
class AuthMixin:
def login(self):
"""Login to Garmin Connect, handling MFA."""
logger.info(f"Starting login process for Garmin user: {self.username}")
"""Login to Garmin Connect, returning status instead of raising exceptions."""
logger.info(f"Starting login for: {self.username}")
try:
# result1 is status, result2 is the mfa_state dict or tokens
result1, result2 = garth.login(self.username, self.password, return_on_mfa=True)
if result1 == "needs_mfa":
logger.info("MFA required for Garmin authentication.")
self.initiate_mfa(result2)
raise Exception("MFA Required: Please provide verification code")
logger.info(f"Successfully logged in to Garmin Connect as {self.username}")
self.initiate_mfa(result2) # Fixed below
return "mfa_required"
self.update_tokens(result1, result2)
self.is_connected = True
return "success"
except GarthException as e:
logger.error(f"GarthException during login for {self.username}: {e}")
raise Exception(f"Garmin authentication failed: {e}")
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_by(token_type='garmin').first()
if not token_record:
token_record = APIToken(token_type='garmin')
session.add(token_record)
token_record.mfa_state = json.dumps(mfa_state)
token_record.mfa_expires_at = datetime.now() + timedelta(minutes=10)
session.commit()
logger.info(f"MFA state saved for user: {self.username}")
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_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:
raise Exception("MFA session expired.")
mfa_state = json.loads(token_record.mfa_state)
try:
oauth1, oauth2 = garth.resume_login(mfa_state, verification_code)
self.update_tokens(oauth1, oauth2)
token_record.mfa_state = None
token_record.mfa_expires_at = None
session.commit()
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
logger.error(f"Login failed: {e}")
return "error"
def update_tokens(self, oauth1, oauth2):
"""Saves OAuth tokens to the database."""
logger.info(f"Updating tokens for user: {self.username}")
"""Saves the Garmin OAuth tokens to the database."""
logger.info(f"Updating Garmin 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()
@@ -90,10 +42,63 @@ class AuthMixin:
token_record.garth_oauth1_token = json.dumps(oauth1)
token_record.garth_oauth2_token = json.dumps(oauth2)
token_record.updated_at = datetime.now()
# Clear MFA state as it's no longer needed
token_record.mfa_state = None
token_record.mfa_expires_at = None
session.commit()
logger.info(f"Tokens successfully updated for user: {self.username}")
logger.info("Garmin tokens updated successfully.")
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)
def initiate_mfa(self, mfa_state):
"""Saves ONLY serializable parts of the MFA state to the database."""
logger.info(f"Initiating MFA process for user: {self.username}")
# FIX: Extract serializable data. We cannot dump the 'client' object directly.
serializable_state = {
"signin_params": mfa_state["signin_params"],
"cookies": mfa_state["client"].sess.cookies.get_dict(),
"domain": mfa_state["client"].domain
}
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)
# Save the dictionary as a string
token_record.mfa_state = json.dumps(serializable_state)
token_record.mfa_expires_at = datetime.now() + timedelta(minutes=10)
session.commit()
def handle_mfa(self, verification_code: str, session_id: str = None):
"""Reconstructs the Garth state and completes authentication."""
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 or not token_record.mfa_state:
raise Exception("No pending MFA session found.")
saved_data = json.loads(token_record.mfa_state)
# FIX: Reconstruct the Garth Client and State object
from garth.http import Client
client = Client(domain=saved_data["domain"])
client.sess.cookies.update(saved_data["cookies"])
mfa_state = {
"client": client,
"signin_params": saved_data["signin_params"]
}
try:
oauth1, oauth2 = garth.resume_login(mfa_state, verification_code)
self.update_tokens(oauth1, oauth2)
# ... rest of your session cleanup ...
return True
except GarthException as e:
logger.error(f"MFA handling failed: {e}")
raise

View File

@@ -32,3 +32,11 @@ class GarminClient(AuthMixin, DataMixin):
except:
self.is_connected = False
return False
def get_profile_info(self):
"""Get user profile information."""
if not self.is_connected:
self.login()
if self.is_connected:
return garth.UserProfile.get()
return None