2026-07-17 19:03:29 +02:00
|
|
|
package api
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"errors"
|
|
|
|
|
"net/http"
|
|
|
|
|
|
2026-07-24 21:08:07 +02:00
|
|
|
"geniusrun/backend/internal/store"
|
2026-07-17 19:03:29 +02:00
|
|
|
)
|
|
|
|
|
|
2026-07-17 19:36:51 +02:00
|
|
|
// validateProfile checks the HR zones are ascending and gap-free once they
|
|
|
|
|
// start, but deliberately does NOT require zone 1 to start at 0% or zone 5
|
|
|
|
|
// to end at 100%: real Karvonen HR training zones (e.g. the seeded defaults,
|
|
|
|
|
// 50-60/60-70/70-80/80-90/90-100) don't cover the 0-50% range at all --
|
|
|
|
|
// below zone 1 simply isn't a named training zone.
|
2026-07-17 19:03:29 +02:00
|
|
|
func validateProfile(p store.Profile) error {
|
|
|
|
|
if p.RestingHeartRate != nil && p.MaxHeartRate != nil && *p.RestingHeartRate >= *p.MaxHeartRate {
|
|
|
|
|
return errors.New("resting heart rate must be less than max heart rate")
|
|
|
|
|
}
|
|
|
|
|
zones := [][2]float64{
|
|
|
|
|
{p.HRZone1MinPct, p.HRZone1MaxPct},
|
|
|
|
|
{p.HRZone2MinPct, p.HRZone2MaxPct},
|
|
|
|
|
{p.HRZone3MinPct, p.HRZone3MaxPct},
|
|
|
|
|
{p.HRZone4MinPct, p.HRZone4MaxPct},
|
|
|
|
|
{p.HRZone5MinPct, p.HRZone5MaxPct},
|
|
|
|
|
}
|
|
|
|
|
for i, z := range zones {
|
|
|
|
|
if z[0] >= z[1] {
|
|
|
|
|
return errors.New("each HR zone's min must be less than its max")
|
|
|
|
|
}
|
|
|
|
|
if i > 0 && z[0] != zones[i-1][1] {
|
|
|
|
|
return errors.New("HR zones must be contiguous and non-overlapping")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Server) handleGetProfile(w http.ResponseWriter, r *http.Request) {
|
2026-07-25 18:16:06 +02:00
|
|
|
userID := userIDFromContext(r.Context())
|
|
|
|
|
p, err := s.DB.GetProfile(r.Context(), userID)
|
2026-07-17 19:03:29 +02:00
|
|
|
if err != nil {
|
|
|
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
writeJSON(w, http.StatusOK, p)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Server) handleUpdateProfile(w http.ResponseWriter, r *http.Request) {
|
2026-07-25 18:16:06 +02:00
|
|
|
userID := userIDFromContext(r.Context())
|
2026-07-17 19:03:29 +02:00
|
|
|
var p store.Profile
|
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
|
|
|
|
|
writeError(w, http.StatusBadRequest, "invalid request body")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err := validateProfile(p); err != nil {
|
|
|
|
|
writeError(w, http.StatusBadRequest, err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-25 18:16:06 +02:00
|
|
|
if err := s.DB.UpdateProfile(r.Context(), userID, p); err != nil {
|
2026-07-17 19:03:29 +02:00
|
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-07-25 18:16:06 +02:00
|
|
|
client, err := s.garminFor(r.Context(), userID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
client.UpdateCredentials(p.GarminEmail, p.GarminPassword)
|
2026-07-17 19:03:29 +02:00
|
|
|
|
2026-07-25 18:16:06 +02:00
|
|
|
updated, err := s.DB.GetProfile(r.Context(), userID)
|
2026-07-17 19:03:29 +02:00
|
|
|
if err != nil {
|
|
|
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
writeJSON(w, http.StatusOK, updated)
|
|
|
|
|
}
|