package server import ( "net/http" "github.com/gin-gonic/gin" "scrabble/backend/internal/engine" ) // The /api/v1/user/* endpoints require X-User-ID (RequireUserID middleware). The // backend treats that header as the sole identity input. // handleProfile returns the authenticated account's own profile. func (s *Server) handleProfile(c *gin.Context) { uid, ok := userID(c) if !ok { abortBadRequest(c, "missing identity") return } acc, err := s.accounts.GetByID(c.Request.Context(), uid) if err != nil { s.abortErr(c, err) return } c.JSON(http.StatusOK, profileResponseFor(acc)) } // submitPlayRequest places tiles in a direction on the player's turn. Each tile's Letter // is a wire alphabet index (Stage 13); for a blank it is the designated letter's index. type submitPlayRequest struct { Dir string `json:"dir"` Tiles []struct { Row int `json:"row"` Col int `json:"col"` Letter int `json:"letter"` Blank bool `json:"blank"` } `json:"tiles"` } // tilesFromRequest maps a play/evaluate request's index-addressed tiles to engine tile // records for the game's variant (Stage 13: a placed blank carries its designated letter's // index with Blank set). An out-of-range index surfaces as engine.ErrIllegalPlay (HTTP 400). func tilesFromRequest(variant engine.Variant, req submitPlayRequest) ([]engine.TileRecord, error) { tiles := make([]engine.TileRecord, 0, len(req.Tiles)) for _, t := range req.Tiles { letter, err := engine.LetterForIndex(variant, t.Letter) if err != nil { return nil, err } tiles = append(tiles, engine.TileRecord{Row: t.Row, Col: t.Col, Letter: letter, Blank: t.Blank}) } return tiles, nil } // handleSubmitPlay validates, scores and commits a placement. func (s *Server) handleSubmitPlay(c *gin.Context) { uid, ok := userID(c) if !ok { abortBadRequest(c, "missing identity") return } gameID, ok := gameIDParam(c) if !ok { abortBadRequest(c, "invalid game id") return } var req submitPlayRequest if err := c.ShouldBindJSON(&req); err != nil { abortBadRequest(c, "invalid request body") return } dir, ok := parseDirection(req.Dir) if !ok { abortBadRequest(c, "dir must be H or V") return } variant, err := s.games.GameVariant(c.Request.Context(), gameID) if err != nil { s.abortErr(c, err) return } tiles, err := tilesFromRequest(variant, req) if err != nil { s.abortErr(c, err) return } res, err := s.games.SubmitPlay(c.Request.Context(), gameID, uid, dir, tiles) if err != nil { s.abortErr(c, err) return } s.writeMoveResult(c, res) } // handleGameState returns the player's view of a game. func (s *Server) handleGameState(c *gin.Context) { uid, ok := userID(c) if !ok { abortBadRequest(c, "missing identity") return } gameID, ok := gameIDParam(c) if !ok { abortBadRequest(c, "invalid game id") return } view, err := s.games.GameState(c.Request.Context(), gameID, uid) if err != nil { s.abortErr(c, err) return } dto, err := stateDTOFrom(view, c.Query("include_alphabet") == "true") if err != nil { s.abortErr(c, err) return } s.fillSeatNames(c.Request.Context(), &dto.Game, map[string]string{}) c.JSON(http.StatusOK, dto) } // enqueueRequest joins the per-variant auto-match pool. type enqueueRequest struct { Variant string `json:"variant"` } // handleEnqueue joins the auto-match pool for a variant. func (s *Server) handleEnqueue(c *gin.Context) { uid, ok := userID(c) if !ok { abortBadRequest(c, "missing identity") return } var req enqueueRequest if err := c.ShouldBindJSON(&req); err != nil { abortBadRequest(c, "invalid request body") return } variant, err := engine.ParseVariant(req.Variant) if err != nil { abortBadRequest(c, "unknown variant") return } res, err := s.matchmaker.Enqueue(c.Request.Context(), uid, variant) if err != nil { s.abortErr(c, err) return } dto := matchDTOFrom(res) if dto.Game != nil { s.fillSeatNames(c.Request.Context(), dto.Game, map[string]string{}) } c.JSON(http.StatusOK, dto) } // handleCancel removes the caller from the auto-match pool (and drops any pending // matched result), so a cancelled quick-match neither blocks a re-queue nor later // surfaces a robot-substituted game the player abandoned. It is idempotent: cancelling // when not queued is a no-op success. func (s *Server) handleCancel(c *gin.Context) { uid, ok := userID(c) if !ok { abortBadRequest(c, "missing identity") return } s.matchmaker.Cancel(c.Request.Context(), uid) c.Status(http.StatusNoContent) } // handlePoll reports whether the caller has been paired since queueing. func (s *Server) handlePoll(c *gin.Context) { uid, ok := userID(c) if !ok { abortBadRequest(c, "missing identity") return } res, err := s.matchmaker.Poll(c.Request.Context(), uid) if err != nil { s.abortErr(c, err) return } dto := matchDTOFrom(res) if dto.Game != nil { s.fillSeatNames(c.Request.Context(), dto.Game, map[string]string{}) } c.JSON(http.StatusOK, dto) } // chatPostRequest posts a per-game chat message. type chatPostRequest struct { Body string `json:"body"` } // handleChatPost stores a chat message from the authenticated player. The sender // IP is taken from the gateway-forwarded X-Forwarded-For header. func (s *Server) handleChatPost(c *gin.Context) { uid, ok := userID(c) if !ok { abortBadRequest(c, "missing identity") return } gameID, ok := gameIDParam(c) if !ok { abortBadRequest(c, "invalid game id") return } var req chatPostRequest if err := c.ShouldBindJSON(&req); err != nil { abortBadRequest(c, "invalid request body") return } msg, err := s.social.PostMessage(c.Request.Context(), gameID, uid, req.Body, clientIP(c)) if err != nil { s.abortErr(c, err) return } c.JSON(http.StatusOK, chatDTOFrom(msg)) }