package integration import ( "encoding/json" "log" "net/http" "strconv" "rideaware/internal/config" "rideaware/internal/middleware" ) type GarminHandler struct { client *GarminClient oauthService *OAuthService } func NewGarminHandler() *GarminHandler { return &GarminHandler{ client: NewGarminClient(), oauthService: NewOAuthService(), } } // StartAuth GET /api/protected/garmin/auth - initiates Garmin OAuth2 PKCE flow func (h *GarminHandler) StartAuth(w http.ResponseWriter, r *http.Request) { claims := r.Context().Value(middleware.UserContextKey).(*config.CustomClaims) if claims == nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(map[string]string{"error": "unauthorized"}) return } authURL, err := h.client.BuildAuthURL(claims.UserID) if err != nil { log.Printf("Garmin auth error: %v", err) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]string{"auth_url": authURL}) } // Callback GET /api/garmin/callback - handles Garmin OAuth callback (public endpoint) func (h *GarminHandler) Callback(w http.ResponseWriter, r *http.Request) { code := r.URL.Query().Get("code") state := r.URL.Query().Get("state") if code == "" || state == "" { errMsg := r.URL.Query().Get("error") if errMsg == "" { errMsg = "missing code or state parameter" } appURL := config.OAuth.AppURL http.Redirect(w, r, appURL+"/settings?garmin=error&message="+errMsg, http.StatusFound) return } _, err := h.client.HandleCallback(code, state) if err != nil { log.Printf("Garmin callback error: %v", err) appURL := config.OAuth.AppURL http.Redirect(w, r, appURL+"/settings?garmin=error&message=auth_failed", http.StatusFound) return } appURL := config.OAuth.AppURL http.Redirect(w, r, appURL+"/settings?garmin=connected", http.StatusFound) } // PushWorkout POST /api/protected/workouts/push/garmin?id=X - pushes workout to Garmin Connect func (h *GarminHandler) PushWorkout(w http.ResponseWriter, r *http.Request) { claims := r.Context().Value(middleware.UserContextKey).(*config.CustomClaims) if claims == nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(map[string]string{"error": "unauthorized"}) return } idStr := r.URL.Query().Get("id") if idStr == "" { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(map[string]string{"error": "workout id is required"}) return } id, err := strconv.ParseUint(idStr, 10, 64) if err != nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(map[string]string{"error": "invalid workout id"}) return } if err := h.client.PushWorkout(uint(id), claims.UserID); err != nil { log.Printf("Garmin push error: %v", err) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]string{"message": "workout pushed to Garmin Connect"}) } // ConnectionStatus GET /api/protected/garmin/status - check Garmin connection status func (h *GarminHandler) ConnectionStatus(w http.ResponseWriter, r *http.Request) { claims := r.Context().Value(middleware.UserContextKey).(*config.CustomClaims) if claims == nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(map[string]string{"error": "unauthorized"}) return } status, err := h.oauthService.GetConnectionStatus(claims.UserID, "garmin") if err != nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(status) } // Disconnect DELETE /api/protected/garmin/disconnect - revoke Garmin connection func (h *GarminHandler) Disconnect(w http.ResponseWriter, r *http.Request) { claims := r.Context().Value(middleware.UserContextKey).(*config.CustomClaims) if claims == nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(map[string]string{"error": "unauthorized"}) return } if err := h.oauthService.Disconnect(claims.UserID, "garmin"); err != nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]string{"message": "Garmin disconnected"}) }