diff options
| author | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-08-21 19:02:05 +0200 |
|---|---|---|
| committer | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-08-21 19:02:05 +0200 |
| commit | 1e63f5c91f0e5ff7dfc582b67d262d5939cc34bc (patch) | |
| tree | 7965a98e4bd286df502d658e687fa83230895c39 /web/strava.go | |
| parent | 9ec4d3dbc1f55ca412f6df67b8576b087b15887c (diff) | |
Add global analytics page shell and split server.go by concern
Diffstat (limited to 'web/strava.go')
| -rw-r--r-- | web/strava.go | 144 |
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 +} |
