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,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"`
}