feat: backend service

This commit is contained in:
Ilia Denisov
2026-05-06 10:14:55 +03:00
committed by GitHub
parent 3e2622757e
commit f446c6a2ac
1486 changed files with 49720 additions and 266401 deletions
@@ -0,0 +1,180 @@
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"`
}