refactor(store): single account name on users; rename profile/laps tables

users.display_name becomes users.name and is now the only human-facing
name -- profiles.name is dropped (it duplicated the account name; the
Profile page's Name field now edits users.name through PUT /api/profile,
which carries Name alongside the profiles columns). The profile table
becomes profiles and laps becomes activity_laps, homogeneous with
activity_samples. Onboarding pre-fills the display name from the OIDC
claim's name.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-04 16:11:23 +02:00
parent e2b2bf9611
commit 042579a396
19 changed files with 109 additions and 79 deletions

View File

@@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"net/http"
"strings"
"geniusrun/backend/internal/store"
)
@@ -35,6 +36,14 @@ func validateProfile(p store.Profile) error {
return nil
}
// profilePayload is the profile API's request/response shape: the profiles
// row plus Name, which lives on the users row (the account's single
// human-facing name) but is edited from the same Profile screen.
type profilePayload struct {
store.Profile
Name string
}
func (s *Server) handleGetProfile(w http.ResponseWriter, r *http.Request) {
userID := userIDFromContext(r.Context())
p, err := s.DB.GetProfile(r.Context(), userID)
@@ -42,22 +51,31 @@ func (s *Server) handleGetProfile(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, p)
u, _ := userFromContext(r.Context())
writeJSON(w, http.StatusOK, profilePayload{Profile: p, Name: u.Name})
}
func (s *Server) handleUpdateProfile(w http.ResponseWriter, r *http.Request) {
userID := userIDFromContext(r.Context())
var p store.Profile
var p profilePayload
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
if err := validateProfile(p); err != nil {
if strings.TrimSpace(p.Name) == "" {
writeError(w, http.StatusBadRequest, "name is required")
return
}
if err := validateProfile(p.Profile); err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
if err := s.DB.UpdateProfile(r.Context(), userID, p); err != nil {
if err := s.DB.UpdateProfile(r.Context(), userID, p.Profile); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
if err := s.DB.UpdateUserName(r.Context(), userID, strings.TrimSpace(p.Name)); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
@@ -73,7 +91,7 @@ func (s *Server) handleUpdateProfile(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, updated)
writeJSON(w, http.StatusOK, profilePayload{Profile: updated, Name: strings.TrimSpace(p.Name)})
}
// handleDeleteProfile permanently deletes the signed-in user's entire