package server import ( "net/http" "time" "galaxy/backend/internal/lobby" "galaxy/backend/internal/server/handlers" "galaxy/backend/internal/server/httperr" "github.com/gin-gonic/gin" "github.com/google/uuid" "go.uber.org/zap" ) // AdminGamesHandlers groups the admin-side game-management handlers // under `/api/v1/admin/games/*`. The current implementation ships real implementations // backed by `*lobby.Service` and adds the `Create` handler used by the // new POST /api/v1/admin/games endpoint for public-game creation. type AdminGamesHandlers struct { svc *lobby.Service logger *zap.Logger } // NewAdminGamesHandlers constructs the handler set. svc may be nil — // in that case every handler returns 501 not_implemented. func NewAdminGamesHandlers(svc *lobby.Service, logger *zap.Logger) *AdminGamesHandlers { if logger == nil { logger = zap.NewNop() } return &AdminGamesHandlers{svc: svc, logger: logger.Named("http.admin.games")} } // List handles GET /api/v1/admin/games. func (h *AdminGamesHandlers) List() gin.HandlerFunc { if h.svc == nil { return handlers.NotImplemented("adminGamesList") } return func(c *gin.Context) { page := parsePositiveQueryInt(c.Query("page"), 1) pageSize := parsePositiveQueryInt(c.Query("page_size"), 50) ctx := c.Request.Context() result, err := h.svc.ListAdminGames(ctx, page, pageSize) if err != nil { respondLobbyError(c, h.logger, "admin games list", ctx, err) return } out := adminGameListWire{ Items: make([]lobbyGameDetailWire, 0, len(result.Items)), Page: result.Page, PageSize: result.PageSize, Total: result.Total, } for _, g := range result.Items { out.Items = append(out.Items, lobbyGameDetailToWire(g)) } c.JSON(http.StatusOK, out) } } // Get handles GET /api/v1/admin/games/{game_id}. func (h *AdminGamesHandlers) Get() gin.HandlerFunc { if h.svc == nil { return handlers.NotImplemented("adminGamesGet") } return func(c *gin.Context) { gameID, ok := parseGameIDParam(c) if !ok { return } ctx := c.Request.Context() game, err := h.svc.GetGame(ctx, gameID) if err != nil { respondLobbyError(c, h.logger, "admin games get", ctx, err) return } c.JSON(http.StatusOK, lobbyGameDetailToWire(game)) } } // Create handles POST /api/v1/admin/games — admin-only public-game // creation. The body intentionally omits `visibility`; the handler // hard-codes `visibility=public` and `owner_user_id=NULL`. func (h *AdminGamesHandlers) Create() gin.HandlerFunc { if h.svc == nil { return handlers.NotImplemented("adminGamesCreate") } return func(c *gin.Context) { var req adminGameCreateRequestWire if err := c.ShouldBindJSON(&req); err != nil { httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "request body must be valid JSON") return } enrollmentEndsAt, err := time.Parse(time.RFC3339Nano, req.EnrollmentEndsAt) if err != nil { httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "enrollment_ends_at must be RFC 3339") return } ctx := c.Request.Context() game, err := h.svc.CreateGame(ctx, lobby.CreateGameInput{ OwnerUserID: nil, Visibility: lobby.VisibilityPublic, GameName: req.GameName, Description: req.Description, MinPlayers: req.MinPlayers, MaxPlayers: req.MaxPlayers, StartGapHours: req.StartGapHours, StartGapPlayers: req.StartGapPlayers, EnrollmentEndsAt: enrollmentEndsAt, TurnSchedule: req.TurnSchedule, TargetEngineVersion: req.TargetEngineVersion, }) if err != nil { respondLobbyError(c, h.logger, "admin games create", ctx, err) return } c.JSON(http.StatusCreated, lobbyGameDetailToWire(game)) } } // ForceStart handles POST /api/v1/admin/games/{game_id}/force-start. func (h *AdminGamesHandlers) ForceStart() gin.HandlerFunc { if h.svc == nil { return handlers.NotImplemented("adminGamesForceStart") } return func(c *gin.Context) { gameID, ok := parseGameIDParam(c) if !ok { return } ctx := c.Request.Context() updated, err := h.svc.AdminForceStart(ctx, gameID) if err != nil { respondLobbyError(c, h.logger, "admin games force-start", ctx, err) return } c.JSON(http.StatusAccepted, lobbyGameStateChangeToWire(updated)) } } // ForceStop handles POST /api/v1/admin/games/{game_id}/force-stop. func (h *AdminGamesHandlers) ForceStop() gin.HandlerFunc { if h.svc == nil { return handlers.NotImplemented("adminGamesForceStop") } return func(c *gin.Context) { gameID, ok := parseGameIDParam(c) if !ok { return } ctx := c.Request.Context() updated, err := h.svc.AdminForceStop(ctx, gameID) if err != nil { respondLobbyError(c, h.logger, "admin games force-stop", ctx, err) return } c.JSON(http.StatusOK, lobbyGameStateChangeToWire(updated)) } } // BanMember handles POST /api/v1/admin/games/{game_id}/ban-member. func (h *AdminGamesHandlers) BanMember() gin.HandlerFunc { if h.svc == nil { return handlers.NotImplemented("adminGamesBanMember") } return func(c *gin.Context) { gameID, ok := parseGameIDParam(c) if !ok { return } var req adminGameBanMemberRequestWire if err := c.ShouldBindJSON(&req); err != nil { httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "request body must be valid JSON") return } userID, err := uuid.Parse(req.UserID) if err != nil { httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "user_id must be a valid UUID") return } ctx := c.Request.Context() updated, err := h.svc.AdminBanMember(ctx, gameID, userID, req.Reason) if err != nil { respondLobbyError(c, h.logger, "admin games ban-member", ctx, err) return } c.JSON(http.StatusOK, lobbyMembershipDetailToWire(updated)) } } // adminGameListWire mirrors `AdminGameList`. type adminGameListWire struct { Items []lobbyGameDetailWire `json:"items"` Page int `json:"page"` PageSize int `json:"page_size"` Total int `json:"total"` } // adminGameCreateRequestWire mirrors `AdminGameCreateRequest`. type adminGameCreateRequestWire struct { GameName string `json:"game_name"` Description string `json:"description"` MinPlayers int32 `json:"min_players"` MaxPlayers int32 `json:"max_players"` StartGapHours int32 `json:"start_gap_hours"` StartGapPlayers int32 `json:"start_gap_players"` EnrollmentEndsAt string `json:"enrollment_ends_at"` TurnSchedule string `json:"turn_schedule"` TargetEngineVersion string `json:"target_engine_version"` } // adminGameBanMemberRequestWire mirrors `AdminGameBanMemberRequest`. type adminGameBanMemberRequestWire struct { UserID string `json:"user_id"` Reason string `json:"reason"` }