feat: backend service
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
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"`
|
||||
}
|
||||
Reference in New Issue
Block a user