aboutsummaryrefslogtreecommitdiff
path: root/web/strava.go
diff options
context:
space:
mode:
Diffstat (limited to 'web/strava.go')
-rw-r--r--web/strava.go144
1 files changed, 144 insertions, 0 deletions
diff --git a/web/strava.go b/web/strava.go
new file mode 100644
index 0000000..1e498f5
--- /dev/null
+++ b/web/strava.go
@@ -0,0 +1,144 @@
+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
+}