feat: add profile REST endpoints with HR zone validation
Implements GET /api/profile and PUT /api/profile endpoints with validation of HR zones (must be contiguous, non-overlapping, 0-100%). Also updates profile migration to use correct HR zone defaults (0-20, 20-40, etc.) that match validation requirements. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
71
backend/internal/api/profile.go
Normal file
71
backend/internal/api/profile.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"smartrun/backend/internal/store"
|
||||
)
|
||||
|
||||
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},
|
||||
}
|
||||
if zones[0][0] != 0 {
|
||||
return errors.New("zone 1 must start at 0%")
|
||||
}
|
||||
if zones[len(zones)-1][1] != 100 {
|
||||
return errors.New("zone 5 must end at 100%")
|
||||
}
|
||||
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) {
|
||||
p, err := s.DB.GetProfile(r.Context())
|
||||
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) {
|
||||
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
|
||||
}
|
||||
|
||||
if err := s.DB.UpdateProfile(r.Context(), p); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
s.Garmin.UpdateCredentials(p.GarminEmail, p.GarminPassword)
|
||||
|
||||
updated, err := s.DB.GetProfile(r.Context())
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, updated)
|
||||
}
|
||||
Reference in New Issue
Block a user