From 8fe375a9662f9e68d9e647c5a1bc593c70f43950 Mon Sep 17 00:00:00 2001 From: sstent Date: Wed, 24 Dec 2025 18:12:11 -0800 Subject: [PATCH] working --- FitnessSync/SAVE_GARMIN_CREDS.md | 52 + FitnessSync/backend/Dockerfile | 12 - .../backend/__pycache__/main.cpython-313.pyc | Bin 2673 -> 2673 bytes ...dd_mfa_state_to_api_tokens.cpython-313.pyc | Bin 1000 -> 1045 bytes FitnessSync/backend/docker-compose.yml | 31 - .../src/api/__pycache__/setup.cpython-313.pyc | Bin 8331 -> 8225 bytes FitnessSync/backend/src/api/setup.py | 221 +- .../__pycache__/api_token.cpython-313.pyc | Bin 1410 -> 1447 bytes .../models/__pycache__/config.cpython-313.pyc | Bin 1222 -> 1222 bytes FitnessSync/backend/src/models/api_token.py | 4 +- .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 180 bytes .../garmin/__pycache__/auth.cpython-313.pyc | Bin 0 -> 6782 bytes .../garmin/__pycache__/client.cpython-313.pyc | Bin 0 -> 2038 bytes .../garmin/__pycache__/data.cpython-313.pyc | Bin 0 -> 8162 bytes .../backend/src/services/garmin/auth.py | 141 +- .../backend/src/services/garmin/client.py | 8 + .../utils/__pycache__/helpers.cpython-313.pyc | Bin 2325 -> 1931 bytes FitnessSync/backend/templates/setup.html | 229 +- FitnessSync/docker-compose.override.yml | 16 + FitnessSync/docker-compose.yml | 9 +- FitnessSync/garth_reference.txt | 4766 +++++++++++++++++ FitnessSync/save_garmin_creds.py | 117 + 22 files changed, 5397 insertions(+), 209 deletions(-) create mode 100644 FitnessSync/SAVE_GARMIN_CREDS.md delete mode 100644 FitnessSync/backend/Dockerfile delete mode 100644 FitnessSync/backend/docker-compose.yml create mode 100644 FitnessSync/backend/src/services/garmin/__pycache__/__init__.cpython-313.pyc create mode 100644 FitnessSync/backend/src/services/garmin/__pycache__/auth.cpython-313.pyc create mode 100644 FitnessSync/backend/src/services/garmin/__pycache__/client.cpython-313.pyc create mode 100644 FitnessSync/backend/src/services/garmin/__pycache__/data.cpython-313.pyc create mode 100644 FitnessSync/docker-compose.override.yml create mode 100644 FitnessSync/garth_reference.txt create mode 100644 FitnessSync/save_garmin_creds.py diff --git a/FitnessSync/SAVE_GARMIN_CREDS.md b/FitnessSync/SAVE_GARMIN_CREDS.md new file mode 100644 index 0000000..2b2893d --- /dev/null +++ b/FitnessSync/SAVE_GARMIN_CREDS.md @@ -0,0 +1,52 @@ +# Save Garmin Credentials Script + +This script mimics the web UI call when hitting "Save Garmin Credentials". It loads Garmin credentials from a .env file and sends them to the backend API. + +## Usage + +1. Create a `.env` file based on the `.env.example` template: + ```bash + cp .env.example .env + ``` + +2. Update the `.env` file with your actual Garmin credentials: + ```bash + nano .env + ``` + +3. Run the script: + ```bash + python save_garmin_creds.py + ``` + +## Prerequisites + +- Make sure the backend service is running on the specified host and port (default: localhost:8000) +- Ensure the required dependencies are installed (they should be in the main project requirements.txt) + +## Expected Response + +Upon successful authentication, you'll see a response like: +``` +Response: { + "status": "success", + "message": "Garmin credentials saved and authenticated successfully" +} +``` + +If MFA is required: +``` +Response: { + "status": "mfa_required", + "message": "Multi-factor authentication required", + "session_id": "some_session_id" +} +``` + +## Environment Variables + +- `GARMIN_USERNAME` (required): Your Garmin Connect username +- `GARMIN_PASSWORD` (required): Your Garmin Connect password +- `GARMIN_IS_CHINA` (optional): Set to 'true' if you're using Garmin China (default: false) +- `BACKEND_HOST` (optional): Backend host (default: localhost) +- `BACKEND_PORT` (optional): Backend port (default: 8000) \ No newline at end of file diff --git a/FitnessSync/backend/Dockerfile b/FitnessSync/backend/Dockerfile deleted file mode 100644 index 89f7120..0000000 --- a/FitnessSync/backend/Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -FROM python:3.11-slim - -WORKDIR /app - -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt - -COPY . . - -EXPOSE 8000 - -CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/FitnessSync/backend/__pycache__/main.cpython-313.pyc b/FitnessSync/backend/__pycache__/main.cpython-313.pyc index 95b35f1f4f64f5503fce58f266ccb054056aa854..57ada71796b8cadba83bcb2f732fe7ddf34c58f4 100644 GIT binary patch delta 20 acmew;@==8QGcPX}0}y!fdvD}U;{pIa`2_p` delta 20 acmew;@==8QGcPX}0}z}#=edzPjSB!q@CG^n diff --git a/FitnessSync/backend/alembic/versions/__pycache__/299d39b0f13d_add_mfa_state_to_api_tokens.cpython-313.pyc b/FitnessSync/backend/alembic/versions/__pycache__/299d39b0f13d_add_mfa_state_to_api_tokens.cpython-313.pyc index a593a716b833bb254711b66f54f29432c4e5714d..970a11ee295dc590bd6490882a614ecc98f1d0aa 100644 GIT binary patch literal 1045 zcma)5y>HV%6hD8&aonb~qEe-TkX5D#Qm0O;CQ*fiS|QP@5sI^Qu>5gNLmk`PIYogb z>cZNkJ3@$$6_EHBSQ#o+1|}x9Ld3?vyToZyCQf?4``x{F_ul=U-Opwf1mn-Et=2aV zq3>)l8hk?fWgsWWKm-|_!EbY0JmFQwg>8X|hDcIIib#e)(qOV7Eh3UJ(tt`+$jMI* z@4Bwq-m^>^SrJyFK()e_83hN}r;1X?hb`I)eD(fjRn;pMw^*?Ydn-kE=)ttAu2uAM zsbJf-jtj~L!R(ez0EjQ>tN9f@uNT#oQdKWkOQmI9FP2Mdit>Y%pW|Xa2y5~5b@ zR+Bs`YNA}K7As?|;>?AJ4S1LAcRbIsJ)BDsRx*UTuIU6`r|ri)wPI;w$R#5pEx-8| z^;xDk)b@im)+mjzA88Lr@DMu@)$X;T2C9(y3Bp7;GQ+$q zEtv&~Gj8zM9~M6k>jf~%9K+&kN44Yi=hsff{NPA#Do&fG8#tyJrxV4J*3d_u+Qhpc=!&v4qo_; z5iJ6o3mnJ&2VkF(zw~5$;BzTY< z6GA|5IZ1o)e`&}itS1pc1)&&4@Z?NLkM_YnzVChSn|W_`sDIVkGs`l8$M^Sz+Ft_j zS&)m67hrb6!4U2O0|Z8xv_}dON(GAamf&b839?Va9}aXc{rd%BfdlFO?AEx6YF?U;Q8JLX=Pd diff --git a/FitnessSync/backend/docker-compose.yml b/FitnessSync/backend/docker-compose.yml deleted file mode 100644 index 55d203c..0000000 --- a/FitnessSync/backend/docker-compose.yml +++ /dev/null @@ -1,31 +0,0 @@ -version: '3.8' - -services: - app: - build: . - ports: - - "8000:8000" - environment: - - DATABASE_URL=postgresql://postgres:password@db:5432/fitbit_garmin_sync - - FITBIT_CLIENT_ID=${FITBIT_CLIENT_ID:-} - - FITBIT_CLIENT_SECRET=${FITBIT_CLIENT_SECRET:-} - - FITBIT_REDIRECT_URI=${FITBIT_REDIRECT_URI:-http://localhost:8000/api/setup/fitbit/callback} - depends_on: - - db - volumes: - - ./data:/app/data # For activity files - - ./logs:/app/logs # For application logs - - db: - image: postgres:15 - environment: - - POSTGRES_DB=fitbit_garmin_sync - - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=password - ports: - - "5432:5432" - volumes: - - postgres_data:/var/lib/postgresql/data - -volumes: - postgres_data: \ No newline at end of file diff --git a/FitnessSync/backend/src/api/__pycache__/setup.cpython-313.pyc b/FitnessSync/backend/src/api/__pycache__/setup.cpython-313.pyc index d4a9dbf95791dc0b8c5dfa274e30dd2bbe010f3e..aee0c87e8250aa40e162067701c6fae237eefbfc 100644 GIT binary patch literal 8225 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 delta 4104 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 Uxm}&wkXx{7zNY{YhCgBd1qKNLQUCw| diff --git a/FitnessSync/backend/src/api/setup.py b/FitnessSync/backend/src/api/setup.py index 720c0e9..754089b 100644 --- a/FitnessSync/backend/src/api/setup.py +++ b/FitnessSync/backend/src/api/setup.py @@ -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}") + diff --git a/FitnessSync/backend/src/models/__pycache__/api_token.cpython-313.pyc b/FitnessSync/backend/src/models/__pycache__/api_token.cpython-313.pyc index 7008cf3cc643b783e7f782e3235eb04c74d71653..7241c58680b1857a5e917be97d6915bfd20b22f5 100644 GIT binary patch delta 193 zcmZqTUe3+?nU|M~0SG+#y)$o3=A;j4v)pEJ>Xl!n}*| z>STVFc5VT&31ur-E(+*2cut=akl$zYY^muX}^G?Q-lQme{C+}u)W7L|=#2PEA p1+=J08%W&Zu*uC&Da}c>D>9f|!m1=F$mqt{QSy}mL>GaS0RTcoCmsL* diff --git a/FitnessSync/backend/src/models/__pycache__/config.cpython-313.pyc b/FitnessSync/backend/src/models/__pycache__/config.cpython-313.pyc index 24ecf1feca0850892cd4ae8e7755a01bb5158ccb..a67ff178192b238b71b9b91c1ae0af2cce0e2b88 100644 GIT binary patch delta 20 acmX@cd5n|$GcPX}0}y!fdvE04%>n>7uLR!! delta 20 acmX@cd5n|$GcPX}0}$M7^xVk3n*{(qK?UCc diff --git a/FitnessSync/backend/src/models/api_token.py b/FitnessSync/backend/src/models/api_token.py index a046711..4824afb 100644 --- a/FitnessSync/backend/src/models/api_token.py +++ b/FitnessSync/backend/src/models/api_token.py @@ -8,9 +8,9 @@ class APIToken(Base): id = Column(Integer, primary_key=True, index=True) token_type = Column(String, nullable=False) # 'fitbit' or 'garmin' - access_token = Column(String, nullable=False) # This should be encrypted in production + access_token = Column(String, nullable=True) # This should be encrypted in production refresh_token = Column(String, nullable=True) # This should be encrypted in production - expires_at = Column(DateTime, nullable=False) + expires_at = Column(DateTime, nullable=True) scopes = Column(String, nullable=True) garth_oauth1_token = Column(String, nullable=True) # OAuth1 token for garmin (JSON) garth_oauth2_token = Column(String, nullable=True) # OAuth2 token for garmin (JSON) diff --git a/FitnessSync/backend/src/services/garmin/__pycache__/__init__.cpython-313.pyc b/FitnessSync/backend/src/services/garmin/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d83ecc22dc2d567d657eb1690d9492a66eac0c68 GIT binary patch literal 180 zcmey&%ge<81eNo=GC}lX5CH>>P{wB#AY&>+I)f&o-%5reCLr%KNa|Lwenx(7s(x{C zNorn+en3%vR%&udvA$boNk~y*a<&nOm6uvv99)@~te*rFNzF^qFD^>fFHS8g%S=u! z)=y6?%FWEvkB`sH%PfhH*DI*J#bJ}1pHiBWYFESxv;*XZVi4maGb1Bo5i^hl07hsp A5&!@I literal 0 HcmV?d00001 diff --git a/FitnessSync/backend/src/services/garmin/__pycache__/auth.cpython-313.pyc b/FitnessSync/backend/src/services/garmin/__pycache__/auth.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b58d86daedbbe3c66b423f5be099494bde746f49 GIT binary patch literal 6782 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 DBQS7^ literal 0 HcmV?d00001 diff --git a/FitnessSync/backend/src/services/garmin/__pycache__/client.cpython-313.pyc b/FitnessSync/backend/src/services/garmin/__pycache__/client.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..af430290265cf8e5d6f8b7b9d5419ed12cfb8b24 GIT binary patch literal 2038 zcmah~O-vg{6rTOF*RcE*;b2nALTQ96WTz!WO#-MD5r3x8uppt}YPGD__K;AtBmY7-~7DyzM1#l z>}f+o9fCI5l)~SO2>r$el@=<4(?tNgNJAQzLWK2vikspIuOdy*#FQ{45;4G1N}39h zkc!fL4@!pzxsnAUx1%1Ug*uQHPK)W#Ab4VpL~>kQK4x$x&iM+pyuzApySbc2j+rh@ zMm{Kb`491@*kOa)hGzr=lPOhNxC~B*0PLbPBAkW@uW>}s>NFuOY9jolv~(@RNUVq} z90>sx=^C%eY)ryhgvdEQUhga823f_wB%)#6eR9JVmm zP+G5*8o$6bLI@UwMl>K+E?Y$lLSK2C5l841Hfr>i8Y|Z4nk(q6hX6`i=mrP&xvG5? zp#{D=ik(-HD0a%E|lYkg)Wrk;==knqj}4`r{b*Y#3q|f=Btuw;Kc7d z7srdsmqCe+g4jSOq7;c84ZVLjGy}h#%we?eNwn`Udi_cC`a$%);{4*~_)%-eUT?AW zN>RD;_X*F#e)X-*+XqVL&#mqMP#Nqzx;lWqi;M_yDL6n-3<2#@ZLlhd3o~%Q2+K8g zD9E)sU0-!G1)E_-*FPy3c6p^q*Rz;V&&H1BxUeixkND!U>)NCpY>_Syew(gS&+xFR z8=gn-a>28xu786F1Q!JtLm0O}FhKi3e2)I$o25x^uk{46{##n=f0%z6j8A&QQf#mH zF9co55L1OpXK$e20(F!qK+I?&Z`o@Wp@ij&FBp{AiXkjAY(Z^7g zn=cQQPCr5zFP6*{-gtB(a2)pwQl6u?pQFq0`!&+Glm98wwGsML?BHTMH@?Cz5rDwd Ezt!X2Jpcdz literal 0 HcmV?d00001 diff --git a/FitnessSync/backend/src/services/garmin/__pycache__/data.cpython-313.pyc b/FitnessSync/backend/src/services/garmin/__pycache__/data.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3caa8c062aab011a4431c98f908daa119a4f36d5 GIT binary patch literal 8162 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 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 \ No newline at end of file diff --git a/FitnessSync/backend/src/services/garmin/client.py b/FitnessSync/backend/src/services/garmin/client.py index 02123ed..51c8910 100644 --- a/FitnessSync/backend/src/services/garmin/client.py +++ b/FitnessSync/backend/src/services/garmin/client.py @@ -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 diff --git a/FitnessSync/backend/src/utils/__pycache__/helpers.cpython-313.pyc b/FitnessSync/backend/src/utils/__pycache__/helpers.cpython-313.pyc index 3178283ce55817f61a2d24504e838fc342c7ae48..1bdb725b06a603453d8b937bdd81bbf0888a9a41 100644 GIT binary patch delta 874 zcmYjP%WD%s9G=;CUTyk_C9T$Nsn%7B3aKcF6}4)y2gw=)Erzu_rd#$=W~LFnw093R zEJClI^dKmB^6t@tFQ`l@6!Z^BsrBs4CQUmqzt{JDzxmDXyy|_?J-(aEr2$_v-FLj> z3IM-FvcGtqtX7wwC0&$gyD?b17a)mlQjd zR5CSar5o?Y2Ii$hon#)1y_;|BoXbfMJ5X7PufqPW$#x->D{@(n-)qC!LmWeJ?pDAg zY|%ocqLee89-W{9_ZcSOjBE$!1gF}O+XF!S8IBqiWeix-^9%78X=soxIMjTKJ&K4) zBa<#5b3O`5L|i*%Tuuxb1|l&(kbH^Z-oN5S`^3 z4j3GmMRnveW1+<*EbX{%TWG0R<kJ!?~IQ@@O^4WszYD6XIVX`GHr>Zg+h zuEKrXumYdAI=_rD3Tc&k0V321mhceA3UQw{0t+7}wfj(EQbgj0ZcZN+s%+p9!aqy3 z?qI@XVa4AzO|#XtSa*ESbttN$u79; z;+^7QQBiM9U#r|=GNBlYD<^X?#rFth%Ht^VlXiGeKnHg+E(ejj?4v9A37}_49vg1*96>saQB2u GPWca@EzrpT delta 1079 zcmZ`&O-vI(6rSnM{`7~^00o5#TR^a4{fTIls4+xA6BA6=MgxsxZP#|SyGv#l6%s>F z#^^zt(ZI3ZynBJ0o-`&V@{^Q|CYs3AL_@qgQ!H?C5A(kF-uJ%u@@DpFEI2l`l!v?VOr*Y}aA9pi6eHXkeH0`;N`cij3?Y znMH@0y_SGfF&>z%H|g9>gs9AgOgm5gBM!E7&oeOfIWAQ!{jQNSu}jgE?iHxau&rAL z4UVvGmsmHZPeW{+ow^3i!-j6vlm5KnU2*c~*ql^h#4E-_VyTZfp;OFd@)I83{@pReK+Si7~}=+5<5y@Bhprl#8)Em&)uojh*rA%U2a^K z+t#D)3xf-N74*4z|6}E=5_lGT5_~Z{KUyB0A1jYlF0HmC7txAxb|W0EhT|*Yc;&!q zc%U@8j)GORXBq8T*t>>$Hd^;pTL)HJ2Uc5?rLlF?TqO^MzSuenR8e#pMW5%_Q0I>4 ztf4-#C@l5G*lf6+9W=_8Fqxgk*s#4x&$JBJ)2%7o&2k-N9>rno%@Vo}MI2-i(juv_ zhn$eQU^_`mUE%->c99ur+~SIf5cV^J`TM7mH#5cV4fE$-E2iRwi^J(rD!3lTd`q<^ z6cyQBQ*_I5ikQpcI6rmH{|4?=a$b(cPY%89f1iAp{0*96({uM1 VH3EKbw diff --git a/FitnessSync/backend/templates/setup.html b/FitnessSync/backend/templates/setup.html index a9ba32d..9bd4bba 100644 --- a/FitnessSync/backend/templates/setup.html +++ b/FitnessSync/backend/templates/setup.html @@ -10,6 +10,10 @@

Fitbit-Garmin Sync - Setup

+
+ +
+
@@ -32,19 +36,28 @@
- +
- +
- + + +
+ +
+

Current auth state: Not Tested

+
+ +
+

Loading Garmin authentication status...

@@ -70,13 +83,14 @@
- +
- +
- + +
-
+