working
This commit is contained in:
@@ -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"]
|
||||
Binary file not shown.
Binary file not shown.
@@ -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:
|
||||
Binary file not shown.
@@ -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}")
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
@@ -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
|
||||
|
||||
Binary file not shown.
@@ -10,6 +10,10 @@
|
||||
<div class="container mt-5">
|
||||
<h1>Fitbit-Garmin Sync - Setup</h1>
|
||||
|
||||
<div class="mb-3">
|
||||
<button type="button" class="btn btn-info" id="load-from-consul-btn">Load Config from Consul</button>
|
||||
</div>
|
||||
|
||||
<!-- Current Status Section -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-12">
|
||||
@@ -32,19 +36,28 @@
|
||||
<form id="garmin-credentials-form">
|
||||
<div class="mb-3">
|
||||
<label for="garmin-username" class="form-label">Username</label>
|
||||
<input type="text" class="form-control" id="garmin-username" name="username" required>
|
||||
<input type="text" class="form-control" id="garmin-username" name="username" required autocomplete="username">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="garmin-password" class="form-label">Password</label>
|
||||
<input type="password" class="form-control" id="garmin-password" name="password" required>
|
||||
<input type="password" class="form-control" id="garmin-password" name="password" required autocomplete="current-password">
|
||||
</div>
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" id="garmin-china" name="is_china">
|
||||
<label class="form-check-label" for="garmin-china">Use China domain (garmin.cn)</label>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Save Garmin Credentials</button>
|
||||
<button type="button" class="btn btn-secondary" id="test-garmin-btn">Test Garmin Credentials</button>
|
||||
<button type="submit" class="btn btn-primary" id="save-garmin-btn" disabled>Save Garmin Credentials</button>
|
||||
<button type="button" class="btn btn-info" id="test-garmin-token-btn">Test Current Garmin Token</button>
|
||||
</form>
|
||||
|
||||
<!-- Garmin Authentication Status -->
|
||||
<div id="garmin-auth-status-text" class="mt-3">
|
||||
<p>Current auth state: <span class="badge bg-secondary">Not Tested</span></p>
|
||||
</div>
|
||||
|
||||
<div id="garmin-token-test-result" class="mt-3"></div>
|
||||
|
||||
<!-- Garmin Authentication Status -->
|
||||
<div id="garmin-auth-status" class="mt-3">
|
||||
<p>Loading Garmin authentication status...</p>
|
||||
@@ -70,13 +83,14 @@
|
||||
<form id="fitbit-credentials-form">
|
||||
<div class="mb-3">
|
||||
<label for="fitbit-client-id" class="form-label">Client ID</label>
|
||||
<input type="text" class="form-control" id="fitbit-client-id" name="client_id" required>
|
||||
<input type="text" class="form-control" id="fitbit-client-id" name="client_id" required autocomplete="username">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="fitbit-client-secret" class="form-label">Client Secret</label>
|
||||
<input type="password" class="form-control" id="fitbit-client-secret" name="client_secret" required>
|
||||
<input type="password" class="form-control" id="fitbit-client-secret" name="client_secret" required autocomplete="new-password">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Save Fitbit Credentials</button>
|
||||
<button type="button" class="btn btn-secondary" id="test-fitbit-btn">Test Fitbit Credentials</button>
|
||||
<button type="submit" class="btn btn-primary" id="save-fitbit-btn" disabled>Save Fitbit Credentials</button>
|
||||
</form>
|
||||
<div class="mt-3">
|
||||
<div id="auth-url-container" style="display: none;">
|
||||
@@ -94,7 +108,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="row mt-4" id="fitbit-oauth-flow-section" style="display: none;">
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
@@ -112,17 +126,60 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Load initial status information
|
||||
loadStatusInfo();
|
||||
|
||||
// Setup form event listeners
|
||||
document.getElementById('load-from-consul-btn').addEventListener('click', loadFromConsul);
|
||||
document.getElementById('test-garmin-btn').addEventListener('click', testGarminCredentials);
|
||||
document.getElementById('test-garmin-token-btn').addEventListener('click', testGarminToken);
|
||||
document.getElementById('garmin-credentials-form').addEventListener('submit', saveGarminCredentials);
|
||||
document.getElementById('test-fitbit-btn').addEventListener('click', testFitbitCredentials);
|
||||
document.getElementById('fitbit-credentials-form').addEventListener('submit', saveFitbitCredentials);
|
||||
document.getElementById('fitbit-callback-form').addEventListener('submit', completeFitbitAuth);
|
||||
});
|
||||
|
||||
async function testGarminToken() {
|
||||
const resultDiv = document.getElementById('garmin-token-test-result');
|
||||
resultDiv.innerHTML = '<p>Testing token...</p>';
|
||||
try {
|
||||
const response = await fetch('/api/setup/garmin/test-token', { method: 'POST' });
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
resultDiv.innerHTML = `<div class="alert alert-success"><pre>${JSON.stringify(data, null, 2)}</pre></div>`;
|
||||
} else {
|
||||
resultDiv.innerHTML = `<div class="alert alert-danger">${data.detail || 'Failed to test token'}</div>`;
|
||||
}
|
||||
} catch (error) {
|
||||
resultDiv.innerHTML = `<div class="alert alert-danger">Error: ${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadFromConsul() {
|
||||
alert('Attempting to load config from Consul and save to backend...');
|
||||
console.log('loadFromConsul function called');
|
||||
try {
|
||||
const response = await fetch('/api/setup/load-consul-config', { method: 'POST' });
|
||||
console.log('Response received:', response);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.detail || 'Failed to load config from Consul');
|
||||
}
|
||||
const data = await response.json();
|
||||
if (data.status === "mfa_required") {
|
||||
alert(data.message);
|
||||
loadStatusInfo(); // Refresh the status info to potentially show MFA section
|
||||
} else {
|
||||
alert(data.message || 'Configuration loaded from Consul successfully.');
|
||||
loadStatusInfo(); // Refresh the status info
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading config from Consul:', error);
|
||||
alert('Error loading config from Consul: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadStatusInfo() {
|
||||
try {
|
||||
@@ -156,16 +213,26 @@
|
||||
// Update Garmin auth status
|
||||
const garminStatusContainer = document.getElementById('garmin-auth-status');
|
||||
if (authData.garmin) {
|
||||
garminStatusContainer.innerHTML = `
|
||||
<div class="alert ${authData.garmin.authenticated ? 'alert-success' : 'alert-warning'}">
|
||||
<h6>Garmin Authentication Status</h6>
|
||||
<p><strong>Username:</strong> ${authData.garmin.username || 'Not set'}</p>
|
||||
<p><strong>Authenticated:</strong> ${authData.garmin.authenticated ? 'Yes' : 'No'}</p>
|
||||
${authData.garmin.token_expires_at ? `<p><strong>Token Expires:</strong> ${new Date(authData.garmin.token_expires_at).toLocaleString()}</p>` : ''}
|
||||
${authData.garmin.last_login ? `<p><strong>Last Login:</strong> ${new Date(authData.garmin.last_login).toLocaleString()}</p>` : ''}
|
||||
<p><strong>Domain:</strong> ${authData.garmin.is_china ? 'garmin.cn' : 'garmin.com'}</p>
|
||||
</div>
|
||||
`;
|
||||
let garminStatusHtml = `<h6>Garmin Authentication Status</h6>`;
|
||||
if (authData.garmin.token_stored) {
|
||||
garminStatusHtml += `<p><strong>Tokens Stored:</strong> Yes</p>`;
|
||||
garminStatusHtml += `<p><strong>Authenticated:</strong> ${authData.garmin.authenticated ? 'Yes' : 'No'}</p>`;
|
||||
garminStatusHtml += `<p><strong>OAuth1 Token Exists:</strong> ${authData.garmin.garth_oauth1_token_exists ? 'Yes' : 'No'}</p>`;
|
||||
garminStatusHtml += `<p><strong>OAuth2 Token Exists:</strong> ${authData.garmin.garth_oauth2_token_exists ? 'Yes' : 'No'}</p>`;
|
||||
garminStatusHtml += `<p><strong>MFA State Exists:</strong> ${authData.garmin.mfa_state_exists ? 'Yes' : 'No'}</p>`;
|
||||
if (authData.garmin.mfa_expires_at) {
|
||||
garminStatusHtml += `<p><strong>MFA Expires:</strong> ${new Date(authData.garmin.mfa_expires_at).toLocaleString()}</p>`;
|
||||
}
|
||||
if (authData.garmin.last_used) {
|
||||
garminStatusHtml += `<p><strong>Last Used:</strong> ${new Date(authData.garmin.last_used).toLocaleString()}</p>`;
|
||||
}
|
||||
if (authData.garmin.updated_at) {
|
||||
garminStatusHtml += `<p><strong>Last Updated:</strong> ${new Date(authData.garmin.updated_at).toLocaleString()}</p>`;
|
||||
}
|
||||
} else {
|
||||
garminStatusHtml += `<p><strong>Tokens Stored:</strong> No</p>`;
|
||||
}
|
||||
garminStatusContainer.innerHTML = `<div class="alert ${authData.garmin.authenticated ? 'alert-success' : 'alert-warning'}">${garminStatusHtml}</div>`;
|
||||
}
|
||||
|
||||
// Update Fitbit auth status
|
||||
@@ -187,6 +254,49 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function testGarminCredentials() {
|
||||
const form = document.getElementById('garmin-credentials-form');
|
||||
const formData = new FormData(form);
|
||||
const credentials = {
|
||||
username: formData.get('username'),
|
||||
password: formData.get('password'),
|
||||
is_china: formData.get('is_china') === 'on' || formData.get('is_china') === 'true'
|
||||
};
|
||||
const statusText = document.getElementById('garmin-auth-status-text');
|
||||
const saveBtn = document.getElementById('save-garmin-btn');
|
||||
|
||||
statusText.innerHTML = `<p>Current auth state: <span class="badge bg-info">Testing...</span></p>`;
|
||||
saveBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/setup/garmin', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(credentials)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === 'mfa_required') {
|
||||
statusText.innerHTML = `<p>Current auth state: <span class="badge bg-warning">MFA Required</span></p>`;
|
||||
document.getElementById('garmin-mfa-section').style.display = 'block';
|
||||
window.garmin_mfa_session_id = data.session_id;
|
||||
alert('Multi-factor authentication required. Please enter the verification code.');
|
||||
} else if (response.ok) {
|
||||
statusText.innerHTML = `<p>Current auth state: <span class="badge bg-success">Authentication Successful</span></p>`;
|
||||
saveBtn.disabled = false;
|
||||
alert('Garmin authentication successful. You can now save the credentials.');
|
||||
} else {
|
||||
statusText.innerHTML = `<p>Current auth state: <span class="badge bg-danger">Authentication Failed</span></p>`;
|
||||
alert(data.message || 'Garmin authentication failed.');
|
||||
}
|
||||
} catch (error) {
|
||||
statusText.innerHTML = `<p>Current auth state: <span class="badge bg-danger">Error</span></p>`;
|
||||
console.error('Error testing Garmin credentials:', error);
|
||||
alert('Error testing Garmin credentials: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveGarminCredentials(event) {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -200,28 +310,17 @@
|
||||
try {
|
||||
const response = await fetch('/api/setup/garmin', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(credentials)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === 'mfa_required') {
|
||||
// Show MFA section if MFA is required and store session ID
|
||||
document.getElementById('garmin-mfa-section').style.display = 'block';
|
||||
// Store the session ID for later use when submitting the MFA code
|
||||
window.garmin_mfa_session_id = data.session_id;
|
||||
alert('Multi-factor authentication required. Please enter the verification code sent to your device.');
|
||||
} else {
|
||||
alert(data.message || 'Garmin credentials saved successfully');
|
||||
|
||||
// Refresh status after saving
|
||||
if (response.ok) {
|
||||
alert('Garmin credentials saved successfully');
|
||||
loadStatusInfo();
|
||||
|
||||
// Hide MFA section if showing
|
||||
document.getElementById('garmin-mfa-section').style.display = 'none';
|
||||
} else {
|
||||
alert(data.message || 'Error saving Garmin credentials.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving Garmin credentials:', error);
|
||||
@@ -229,6 +328,42 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function testFitbitCredentials() {
|
||||
const form = document.getElementById('fitbit-credentials-form');
|
||||
const formData = new FormData(form);
|
||||
const credentials = {
|
||||
client_id: formData.get('client_id'),
|
||||
client_secret: formData.get('client_secret')
|
||||
};
|
||||
const saveBtn = document.getElementById('save-fitbit-btn');
|
||||
|
||||
saveBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/setup/fitbit', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(credentials)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
const authLink = document.getElementById('auth-link');
|
||||
authLink.href = data.auth_url;
|
||||
document.getElementById('auth-url-container').style.display = 'block';
|
||||
document.getElementById('fitbit-oauth-flow-section').style.display = 'block';
|
||||
saveBtn.disabled = false;
|
||||
alert('Fitbit credentials seem valid. You can now save them and proceed with authorization.');
|
||||
} else {
|
||||
alert(data.message || 'Fitbit credentials test failed.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error testing Fitbit credentials:', error);
|
||||
alert('Error testing Fitbit credentials: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveFitbitCredentials(event) {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -248,12 +383,12 @@
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
alert(data.message || 'Fitbit credentials saved successfully');
|
||||
|
||||
// Show the authorization link
|
||||
const authLink = document.getElementById('auth-link');
|
||||
authLink.href = data.auth_url;
|
||||
document.getElementById('auth-url-container').style.display = 'block';
|
||||
if(response.ok) {
|
||||
alert('Fitbit credentials saved successfully');
|
||||
loadStatusInfo();
|
||||
} else {
|
||||
alert(data.message || 'Error saving Fitbit credentials.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving Fitbit credentials:', error);
|
||||
alert('Error saving Fitbit credentials: ' + error.message);
|
||||
@@ -293,12 +428,16 @@
|
||||
|
||||
async function submitMFA() {
|
||||
const mfaCode = document.getElementById('mfa-code').value.trim();
|
||||
|
||||
const statusText = document.getElementById('garmin-auth-status-text');
|
||||
const saveBtn = document.getElementById('save-garmin-btn');
|
||||
|
||||
if (!mfaCode) {
|
||||
alert('Please enter the verification code');
|
||||
return;
|
||||
}
|
||||
|
||||
statusText.innerHTML = `<p>Current auth state: <span class="badge bg-info">Verifying MFA...</span></p>`;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/setup/garmin/mfa', {
|
||||
method: 'POST',
|
||||
@@ -314,18 +453,18 @@
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
statusText.innerHTML = `<p>Current auth state: <span class="badge bg-success">MFA Verification Successful</span></p>`;
|
||||
saveBtn.disabled = false;
|
||||
alert(data.message || 'MFA verification successful');
|
||||
document.getElementById('garmin-mfa-section').style.display = 'none';
|
||||
|
||||
// Clear the MFA code field
|
||||
document.getElementById('mfa-code').value = '';
|
||||
|
||||
// Refresh status after MFA
|
||||
loadStatusInfo();
|
||||
} else {
|
||||
statusText.innerHTML = `<p>Current auth state: <span class="badge bg-danger">MFA Verification Failed</span></p>`;
|
||||
alert('MFA verification failed: ' + data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
statusText.innerHTML = `<p>Current auth state: <span class="badge bg-danger">Error</span></p>`;
|
||||
console.error('Error submitting MFA code:', error);
|
||||
alert('Error submitting MFA code: ' + error.message);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user