initial commit
All checks were successful
pedestrian-simulator / build (push) Successful in 53s

This commit is contained in:
2026-01-11 17:16:59 -07:00
parent c5d00dc9a9
commit 24ecddd034
18 changed files with 4506 additions and 4 deletions

296
server/fitbit.go Normal file
View File

@@ -0,0 +1,296 @@
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)
}