Files
developer 01485d8fc6
Tests · Go / test (push) Successful in 7s
Tests · Integration / integration (push) Successful in 11s
Tests · UI / test (push) Successful in 18s
Stage 11: account linking & merge (email + Telegram Login Widget) (#12)
2026-06-04 09:18:17 +00:00

109 lines
3.1 KiB
Go

package server
import (
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
"scrabble/backend/internal/account"
)
// The /api/v1/user account handlers wire profile editing, email binding and the
// statistics read (Stage 8). They follow handlers_user.go: X-User-ID identity, a
// domain call, a JSON DTO. Profile editing overwrites the full editable set, so the
// client sends the complete desired profile.
// updateProfileRequest is the full editable profile. away_start/away_end are
// "HH:MM" local-time bounds of the daily away window.
type updateProfileRequest struct {
DisplayName string `json:"display_name"`
PreferredLanguage string `json:"preferred_language"`
TimeZone string `json:"time_zone"`
AwayStart string `json:"away_start"`
AwayEnd string `json:"away_end"`
BlockChat bool `json:"block_chat"`
BlockFriendRequests bool `json:"block_friend_requests"`
NotificationsInAppOnly bool `json:"notifications_in_app_only"`
}
// statsDTO is a durable account's lifetime statistics (the derived games-played and
// win-rate are computed client-side).
type statsDTO struct {
Wins int `json:"wins"`
Losses int `json:"losses"`
Draws int `json:"draws"`
MaxGamePoints int `json:"max_game_points"`
MaxWordPoints int `json:"max_word_points"`
}
// parseAwayTime parses an "HH:MM" away-window bound.
func parseAwayTime(s string) (time.Time, bool) {
t, err := time.Parse(awayTimeLayout, strings.TrimSpace(s))
if err != nil {
return time.Time{}, false
}
return t, true
}
// handleUpdateProfile overwrites the caller's editable profile fields.
func (s *Server) handleUpdateProfile(c *gin.Context) {
uid, ok := userID(c)
if !ok {
abortBadRequest(c, "missing identity")
return
}
var req updateProfileRequest
if err := c.ShouldBindJSON(&req); err != nil {
abortBadRequest(c, "invalid request body")
return
}
awayStart, ok := parseAwayTime(req.AwayStart)
if !ok {
abortBadRequest(c, "away_start must be HH:MM")
return
}
awayEnd, ok := parseAwayTime(req.AwayEnd)
if !ok {
abortBadRequest(c, "away_end must be HH:MM")
return
}
acc, err := s.accounts.UpdateProfile(c.Request.Context(), uid, account.ProfileUpdate{
DisplayName: req.DisplayName,
PreferredLanguage: req.PreferredLanguage,
TimeZone: req.TimeZone,
AwayStart: awayStart,
AwayEnd: awayEnd,
BlockChat: req.BlockChat,
BlockFriendRequests: req.BlockFriendRequests,
NotificationsInAppOnly: req.NotificationsInAppOnly,
})
if err != nil {
s.abortErr(c, err)
return
}
c.JSON(http.StatusOK, profileResponseFor(acc))
}
// handleStats returns the caller's lifetime statistics.
func (s *Server) handleStats(c *gin.Context) {
uid, ok := userID(c)
if !ok {
abortBadRequest(c, "missing identity")
return
}
st, err := s.accounts.GetStats(c.Request.Context(), uid)
if err != nil {
s.abortErr(c, err)
return
}
c.JSON(http.StatusOK, statsDTO{
Wins: st.Wins,
Losses: st.Losses,
Draws: st.Draws,
MaxGamePoints: st.MaxGamePoints,
MaxWordPoints: st.MaxWordPoints,
})
}