package web import ( "crypto/rand" "encoding/hex" "errors" "fmt" "log/slog" "net/http" "time" "github.com/martinlehoux/biking_home/config" "github.com/martinlehoux/biking_home/strava" ) func (s *Server) handleStravaLogin(w http.ResponseWriter, r *http.Request) { appConfig, err := s.loadConfig() if err != nil { http.Error(w, "failed to load configuration", http.StatusInternalServerError) return } clientID := appConfig.Strava.ClientID clientSecret := appConfig.Strava.ClientSecret if clientID == "" || clientSecret == "" { http.Error(w, "STRAVA_CLIENT_ID and STRAVA_CLIENT_SECRET are required", http.StatusInternalServerError) return } state, err := newOAuthState() if err != nil { http.Error(w, "failed to start OAuth flow", http.StatusInternalServerError) return } returnTo := safeReturnTo(r.URL.Query().Get("return_to")) s.oauthMu.Lock() s.oauthState = state s.returnToURL = returnTo s.oauthMu.Unlock() redirectURI := appConfig.Server.PublicURL + "/strava/callback" http.Redirect(w, r, strava.AuthorizeURLWithState(clientID, redirectURI, state), http.StatusFound) } func (s *Server) handleStravaCallback(w http.ResponseWriter, r *http.Request) { if callbackError := r.URL.Query().Get("error"); callbackError != "" { http.Error(w, "Strava authorization was denied: "+callbackError, http.StatusBadRequest) return } returnTo, ok := s.consumeOAuthState(r.URL.Query().Get("state")) if !ok { http.Error(w, "invalid OAuth state", http.StatusBadRequest) return } code := r.URL.Query().Get("code") if code == "" { http.Error(w, "missing authorization code", http.StatusBadRequest) return } appConfig, err := s.loadConfig() if err != nil { http.Error(w, "failed to load configuration", http.StatusInternalServerError) return } redirectURI := appConfig.Server.PublicURL + "/strava/callback" token, err := strava.ExchangeCode(appConfig.Strava.ClientID, appConfig.Strava.ClientSecret, code, redirectURI) if err != nil { http.Error(w, "failed to exchange Strava authorization code", http.StatusBadGateway) return } appConfig.Strava.AccessToken = token.AccessToken appConfig.Strava.RefreshToken = token.RefreshToken appConfig.Strava.ExpiresAt = token.ExpiresAt.Unix() if err := config.Save(s.configPath, appConfig); err != nil { http.Error(w, "failed to store Strava token", http.StatusInternalServerError) return } slog.Info("Strava authorization completed") http.Redirect(w, r, returnTo, http.StatusSeeOther) } func (s *Server) stravaClient() (*strava.Client, bool, error) { appConfig, err := s.loadConfig() if err != nil { return nil, false, fmt.Errorf("load configuration: %w", err) } return newStravaClient(appConfig) } func (s *Server) loadConfig() (config.Config, error) { return config.Load(s.configPath) } func newStravaClient(appConfig config.Config) (*strava.Client, bool, error) { if appConfig.Strava.ClientID == "" || appConfig.Strava.ClientSecret == "" { return nil, false, errors.New("STRAVA_CLIENT_ID and STRAVA_CLIENT_SECRET are required") } if appConfig.Strava.AccessToken == "" || appConfig.Strava.RefreshToken == "" { return nil, false, nil } var expiresAt time.Time if appConfig.Strava.ExpiresAt != 0 { expiresAt = time.Unix(appConfig.Strava.ExpiresAt, 0) } return strava.NewClient(appConfig.Strava.ClientID, appConfig.Strava.ClientSecret, strava.Token{ AccessToken: appConfig.Strava.AccessToken, RefreshToken: appConfig.Strava.RefreshToken, ExpiresAt: expiresAt, }), true, nil } func (s *Server) saveStravaToken(client *strava.Client) error { appConfig, err := s.loadConfig() if err != nil { return err } token := client.Tokens() appConfig.Strava.AccessToken = token.AccessToken appConfig.Strava.RefreshToken = token.RefreshToken appConfig.Strava.ExpiresAt = token.ExpiresAt.Unix() return config.Save(s.configPath, appConfig) } func (s *Server) hasStravaToken() bool { _, authorized, err := s.stravaClient() return err == nil && authorized } func (s *Server) consumeOAuthState(state string) (string, bool) { s.oauthMu.Lock() defer s.oauthMu.Unlock() if state == "" || state != s.oauthState { return "", false } returnTo := s.returnToURL s.oauthState = "" s.returnToURL = "" return returnTo, true } func newOAuthState() (string, error) { data := make([]byte, 32) if _, err := rand.Read(data); err != nil { return "", err } return hex.EncodeToString(data), nil }