package server import ( "net/http" "galaxy/backend/internal/lobby" "galaxy/backend/internal/server/handlers" "galaxy/backend/internal/server/httperr" "galaxy/backend/internal/server/middleware/userid" "github.com/gin-gonic/gin" "github.com/google/uuid" "go.uber.org/zap" ) // UserLobbyInvitesHandlers groups the invite-lifecycle handlers under // `/api/v1/user/lobby/games/{game_id}/invites/*`. The current implementation ships real // implementations backed by `*lobby.Service`. type UserLobbyInvitesHandlers struct { svc *lobby.Service logger *zap.Logger } // NewUserLobbyInvitesHandlers constructs the handler set. svc may be // nil — in that case every handler returns 501 not_implemented. func NewUserLobbyInvitesHandlers(svc *lobby.Service, logger *zap.Logger) *UserLobbyInvitesHandlers { if logger == nil { logger = zap.NewNop() } return &UserLobbyInvitesHandlers{svc: svc, logger: logger.Named("http.user.lobby.invites")} } // Issue handles POST /api/v1/user/lobby/games/{game_id}/invites. func (h *UserLobbyInvitesHandlers) Issue() gin.HandlerFunc { if h.svc == nil { return handlers.NotImplemented("userLobbyInvitesIssue") } return func(c *gin.Context) { userID, ok := userid.FromContext(c.Request.Context()) if !ok { httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "X-User-ID header is required") return } gameID, ok := parseGameIDParam(c) if !ok { return } var req lobbyInviteIssueRequestWire if err := c.ShouldBindJSON(&req); err != nil { httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "request body must be valid JSON") return } var invitedID *uuid.UUID if req.InvitedUserID != nil && *req.InvitedUserID != "" { parsed, err := uuid.Parse(*req.InvitedUserID) if err != nil { httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "invited_user_id must be a valid UUID") return } invitedID = &parsed } expires, err := parseTimePtrField(req.ExpiresAt, "expires_at") if err != nil { httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, err.Error()) return } ctx := c.Request.Context() raceName := "" if req.RaceName != nil { raceName = *req.RaceName } invite, err := h.svc.IssueInvite(ctx, lobby.IssueInviteInput{ GameID: gameID, InviterUserID: userID, InvitedUserID: invitedID, RaceName: raceName, ExpiresAt: expires, }) if err != nil { respondLobbyError(c, h.logger, "user lobby invites issue", ctx, err) return } c.JSON(http.StatusCreated, lobbyInviteDetailToWire(invite)) } } // Redeem handles POST /api/v1/user/lobby/games/{game_id}/invites/{invite_id}/redeem. func (h *UserLobbyInvitesHandlers) Redeem() gin.HandlerFunc { if h.svc == nil { return handlers.NotImplemented("userLobbyInvitesRedeem") } return func(c *gin.Context) { userID, ok := userid.FromContext(c.Request.Context()) if !ok { httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "X-User-ID header is required") return } gameID, ok := parseGameIDParam(c) if !ok { return } inviteID, ok := parseInviteIDParam(c) if !ok { return } ctx := c.Request.Context() invite, err := h.svc.RedeemInvite(ctx, userID, gameID, inviteID) if err != nil { respondLobbyError(c, h.logger, "user lobby invites redeem", ctx, err) return } c.JSON(http.StatusOK, lobbyInviteDetailToWire(invite)) } } // Decline handles POST /api/v1/user/lobby/games/{game_id}/invites/{invite_id}/decline. func (h *UserLobbyInvitesHandlers) Decline() gin.HandlerFunc { if h.svc == nil { return handlers.NotImplemented("userLobbyInvitesDecline") } return func(c *gin.Context) { userID, ok := userid.FromContext(c.Request.Context()) if !ok { httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "X-User-ID header is required") return } gameID, ok := parseGameIDParam(c) if !ok { return } inviteID, ok := parseInviteIDParam(c) if !ok { return } ctx := c.Request.Context() invite, err := h.svc.DeclineInvite(ctx, userID, gameID, inviteID) if err != nil { respondLobbyError(c, h.logger, "user lobby invites decline", ctx, err) return } c.JSON(http.StatusOK, lobbyInviteDetailToWire(invite)) } } // Revoke handles POST /api/v1/user/lobby/games/{game_id}/invites/{invite_id}/revoke. func (h *UserLobbyInvitesHandlers) Revoke() gin.HandlerFunc { if h.svc == nil { return handlers.NotImplemented("userLobbyInvitesRevoke") } return func(c *gin.Context) { userID, ok := userid.FromContext(c.Request.Context()) if !ok { httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "X-User-ID header is required") return } gameID, ok := parseGameIDParam(c) if !ok { return } inviteID, ok := parseInviteIDParam(c) if !ok { return } ctx := c.Request.Context() caller := userID invite, err := h.svc.RevokeInvite(ctx, &caller, false, gameID, inviteID) if err != nil { respondLobbyError(c, h.logger, "user lobby invites revoke", ctx, err) return } c.JSON(http.StatusOK, lobbyInviteDetailToWire(invite)) } } // lobbyInviteIssueRequestWire mirrors `LobbyInviteIssueRequest`. type lobbyInviteIssueRequestWire struct { InvitedUserID *string `json:"invited_user_id,omitempty"` RaceName *string `json:"race_name,omitempty"` ExpiresAt *string `json:"expires_at,omitempty"` }