package integration import ( "encoding/json" "log" "net/http" "strconv" "rideaware/internal/config" "rideaware/internal/middleware" ) type WahooHandler struct { client *WahooClient oauthService *OAuthService } func NewWahooHandler() *WahooHandler { return &WahooHandler{ client: NewWahooClient(), oauthService: NewOAuthService(), } } // StartAuth GET /api/protected/wahoo/auth - initiates Wahoo OAuth2 flow func (h *WahooHandler) 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("Wahoo 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/wahoo/callback - handles Wahoo OAuth callback (public endpoint) func (h *WahooHandler) 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?wahoo=error&message="+errMsg, http.StatusFound) return } _, err := h.client.HandleCallback(code, state) if err != nil { log.Printf("Wahoo callback error: %v", err) appURL := config.OAuth.AppURL http.Redirect(w, r, appURL+"/settings?wahoo=error&message=auth_failed", http.StatusFound) return } appURL := config.OAuth.AppURL http.Redirect(w, r, appURL+"/settings?wahoo=connected", http.StatusFound) } // PushWorkout POST /api/protected/workouts/push/wahoo?id=X - pushes workout to Wahoo func (h *WahooHandler) 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("Wahoo 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 Wahoo"}) } // ConnectionStatus GET /api/protected/wahoo/status - check Wahoo connection status func (h *WahooHandler) 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, "wahoo") 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/wahoo/disconnect - revoke Wahoo connection func (h *WahooHandler) 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, "wahoo"); 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": "Wahoo disconnected"}) }