package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) // Config structure to hold credentials type FitbitConfig struct { ClientID string ClientSecret string RedirectURI string AccessToken string RefreshToken string ExpiresAt time.Time } var fitbitConfig FitbitConfig const ( scopes = "activity profile" ) func InitFitbit() { fitbitConfig.ClientID = os.Getenv("FITBIT_CLIENT_ID") fitbitConfig.ClientSecret = os.Getenv("FITBIT_CLIENT_SECRET") fitbitConfig.RedirectURI = "http://localhost:8080/auth/callback" if envRedirect := os.Getenv("FITBIT_AUTH_REDIRECT_URI"); envRedirect != "" { fitbitConfig.RedirectURI = envRedirect } } func loadTokens(userID string) (*FitbitConfig, error) { configPath := fmt.Sprintf("data/users/%s/fitbit_tokens.json", userID) data, err := os.ReadFile(configPath) if err != nil { return nil, err } var config FitbitConfig if err := json.Unmarshal(data, &config); err != nil { return nil, err } return &config, nil } func saveTokens(userID string, config *FitbitConfig) error { userDir := fmt.Sprintf("data/users/%s", userID) if err := os.MkdirAll(userDir, 0755); err != nil { return fmt.Errorf("error creating user directory: %w", err) } configPath := fmt.Sprintf("%s/fitbit_tokens.json", userDir) data, err := json.MarshalIndent(config, "", " ") if err != nil { return err } return os.WriteFile(configPath, data, 0600) } // GetDailySteps fetches step count for a specific date (YYYY-MM-DD) for a specific user func GetDailySteps(userID, date string) (int, error) { config, err := loadTokens(userID) if err != nil { return 0, fmt.Errorf("failed to load tokens: %w", err) } // Check expiry if time.Now().After(config.ExpiresAt) { err := refreshTokens(userID, config) if err != nil { return 0, err } } apiURL := fmt.Sprintf("https://api.fitbit.com/1/user/-/activities/date/%s.json", date) req, _ := http.NewRequest("GET", apiURL, nil) req.Header.Set("Authorization", "Bearer "+config.AccessToken) client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Do(req) if err != nil { return 0, err } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) if resp.StatusCode != 200 { return 0, fmt.Errorf("fitbit api error: %s", resp.Status) } var result struct { Summary struct { Steps int `json:"steps"` } `json:"summary"` } if err := json.Unmarshal(body, &result); err != nil { return 0, err } return result.Summary.Steps, nil } func refreshTokens(userID string, config *FitbitConfig) error { // POST https://api.fitbit.com/oauth2/token data := url.Values{} data.Set("grant_type", "refresh_token") data.Set("refresh_token", config.RefreshToken) req, _ := http.NewRequest("POST", "https://api.fitbit.com/oauth2/token", strings.NewReader(data.Encode())) req.SetBasicAuth(fitbitConfig.ClientID, fitbitConfig.ClientSecret) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != 200 { return fmt.Errorf("token refresh failed: %s", resp.Status) } var result struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` ExpiresIn int `json:"expires_in"` } if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return err } config.AccessToken = result.AccessToken config.RefreshToken = result.RefreshToken config.ExpiresAt = time.Now().Add(time.Duration(result.ExpiresIn) * time.Second) return saveTokens(userID, config) } // HandleFitbitAuth redirects user to Fitbit authorization page func HandleFitbitAuth(w http.ResponseWriter, r *http.Request) { if fitbitConfig.ClientID == "" { http.Error(w, "FITBIT_CLIENT_ID not set", http.StatusInternalServerError) return } authURL := fmt.Sprintf( "https://www.fitbit.com/oauth2/authorize?response_type=code&client_id=%s&redirect_uri=%s&scope=%s", url.QueryEscape(fitbitConfig.ClientID), url.QueryEscape(fitbitConfig.RedirectURI), url.QueryEscape(scopes), ) http.Redirect(w, r, authURL, http.StatusTemporaryRedirect) } // HandleFitbitCallback receives the authorization code and exchanges it for tokens func HandleFitbitCallback(w http.ResponseWriter, r *http.Request) { fmt.Println("[OAuth Callback] Started") code := r.URL.Query().Get("code") if code == "" { fmt.Println("[OAuth Callback] ERROR: No authorization code") http.Error(w, "No authorization code received", http.StatusBadRequest) return } fmt.Println("[OAuth Callback] Received authorization code") // Exchange code for tokens fmt.Println("[OAuth Callback] Exchanging code for tokens...") data := url.Values{} data.Set("grant_type", "authorization_code") data.Set("code", code) data.Set("redirect_uri", fitbitConfig.RedirectURI) req, _ := http.NewRequest("POST", "https://api.fitbit.com/oauth2/token", strings.NewReader(data.Encode())) req.SetBasicAuth(fitbitConfig.ClientID, fitbitConfig.ClientSecret) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Printf("[OAuth Callback] ERROR: Token exchange request failed: %v\n", err) http.Error(w, fmt.Sprintf("Token exchange failed: %v", err), http.StatusInternalServerError) return } defer resp.Body.Close() fmt.Printf("[OAuth Callback] Token exchange response: %d\n", resp.StatusCode) if resp.StatusCode != 200 { body, _ := io.ReadAll(resp.Body) fmt.Printf("[OAuth Callback] ERROR: Token exchange failed: %s - %s\n", resp.Status, body) http.Error(w, fmt.Sprintf("Token exchange failed: %s - %s", resp.Status, body), http.StatusInternalServerError) return } var tokenResult struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` ExpiresIn int `json:"expires_in"` UserID string `json:"user_id"` // Fitbit returns user_id in token response } if err := json.NewDecoder(resp.Body).Decode(&tokenResult); err != nil { fmt.Printf("[OAuth Callback] ERROR: Failed to parse token response: %v\n", err) http.Error(w, fmt.Sprintf("Failed to parse token response: %v", err), http.StatusInternalServerError) return } fmt.Printf("[OAuth Callback] Successfully parsed tokens for user: %s\n", tokenResult.UserID) fitbitUserID := tokenResult.UserID if fitbitUserID == "" { fmt.Println("[OAuth Callback] ERROR: No user_id in token response") http.Error(w, "No user_id in token response", http.StatusInternalServerError) return } // Fetch user profile from Fitbit fmt.Println("[OAuth Callback] Fetching user profile from Fitbit...") userID, displayName, avatarURL, err := FetchFitbitUserProfile(tokenResult.AccessToken) if err != nil { fmt.Printf("[OAuth Callback] WARNING: Could not fetch user profile: %v\n", err) // Use user_id from token if profile fetch fails displayName = fitbitUserID avatarURL = "" } else { fmt.Printf("[OAuth Callback] Successfully fetched profile for: %s\n", displayName) // Verify user_id matches if userID != fitbitUserID { fmt.Printf("[OAuth Callback] WARNING: user_id mismatch: token=%s, profile=%s\n", fitbitUserID, userID) } } // Create or update user in registry fmt.Println("[OAuth Callback] Creating/updating user in registry...") if userRegistry == nil { fmt.Println("[OAuth Callback] ERROR: userRegistry is nil!") http.Error(w, "Server configuration error: user registry not initialized", http.StatusInternalServerError) return } _, err = userRegistry.CreateOrUpdateUser(fitbitUserID, displayName, avatarURL) if err != nil { fmt.Printf("[OAuth Callback] ERROR: Failed to create user: %v\n", err) http.Error(w, fmt.Sprintf("Failed to create user: %v", err), http.StatusInternalServerError) return } fmt.Println("[OAuth Callback] User created/updated successfully") // Save Fitbit tokens fmt.Println("[OAuth Callback] Saving Fitbit tokens...") userConfig := &FitbitConfig{ ClientID: fitbitConfig.ClientID, ClientSecret: fitbitConfig.ClientSecret, RedirectURI: fitbitConfig.RedirectURI, AccessToken: tokenResult.AccessToken, RefreshToken: tokenResult.RefreshToken, ExpiresAt: time.Now().Add(time.Duration(tokenResult.ExpiresIn) * time.Second), } if err := saveTokens(fitbitUserID, userConfig); err != nil { fmt.Printf("[OAuth Callback] ERROR: Failed to save tokens: %v\n", err) http.Error(w, fmt.Sprintf("Failed to save tokens: %v", err), http.StatusInternalServerError) return } fmt.Println("[OAuth Callback] Tokens saved successfully") // Create session fmt.Println("[OAuth Callback] Creating session...") session, err := CreateSession(fitbitUserID) if err != nil { fmt.Printf("[OAuth Callback] ERROR: Failed to create session: %v\n", err) http.Error(w, fmt.Sprintf("Failed to create session: %v", err), http.StatusInternalServerError) return } fmt.Println("[OAuth Callback] Session created successfully") // Set session cookie fmt.Println("[OAuth Callback] Setting session cookie...") SetSessionCookie(w, session) fmt.Printf("[OAuth Callback] ✅ SUCCESS! User %s (%s) logged in\n", displayName, fitbitUserID) // Redirect to homepage fmt.Println("[OAuth Callback] Redirecting to homepage") http.Redirect(w, r, "/", http.StatusTemporaryRedirect) }