package publichttp import ( "log/slog" "net/http" "galaxy/lobby/internal/domain/common" "galaxy/lobby/internal/service/listmyracenames" "galaxy/lobby/internal/service/registerracename" ) // Public HTTP route patterns for the race-name surface owned by // (register) and (self-service list). const ( registerRaceNamePath = "/api/v1/lobby/race-names/register" myRaceNamesPath = "/api/v1/lobby/my/race-names" ) // registerRaceNameRoutes binds the public-port race-name routes: // the `lobby.race_name.register` POST and the // `lobby.race_names.list` GET. Both routes require the X-User-ID // header so the actor is always a user; administrators have no // equivalent admin path on the internal port. func registerRaceNameRoutes(mux *http.ServeMux, deps Dependencies, logger *slog.Logger) { h := &raceNameHandlers{ deps: deps, logger: logger.With("component", "public_http.racenames"), } mux.HandleFunc("POST "+registerRaceNamePath, h.handleRegister) mux.HandleFunc("GET "+myRaceNamesPath, h.handleListMy) } type raceNameHandlers struct { deps Dependencies logger *slog.Logger } // registerRaceNameRequest is the JSON shape for // POST /api/v1/lobby/race-names/register. type registerRaceNameRequest struct { RaceName string `json:"race_name"` SourceGameID string `json:"source_game_id"` } // registerRaceNameResponse mirrors `ports.RegisteredName` on the wire. // `registered_at_ms` carries the Unix-millisecond timestamp of the // successful Register commit; idempotent retries return the same value // recorded by the original commit. type registerRaceNameResponse struct { CanonicalKey string `json:"canonical_key"` RaceName string `json:"race_name"` SourceGameID string `json:"source_game_id"` RegisteredAtMs int64 `json:"registered_at_ms"` } // myRaceNamesResponse is the JSON shape for // GET /api/v1/lobby/my/race-names. The three slices are non-nil but // possibly empty so consumers can iterate without a presence check. type myRaceNamesResponse struct { Registered []registeredRaceNameItem `json:"registered"` Pending []pendingRaceNameItem `json:"pending"` Reservations []raceNameReservationItem `json:"reservations"` } // registeredRaceNameItem mirrors `ports.RegisteredName`. It matches the // `RegisteredRaceName` schema field-for-field so the OpenAPI // model can be reused. type registeredRaceNameItem struct { CanonicalKey string `json:"canonical_key"` RaceName string `json:"race_name"` SourceGameID string `json:"source_game_id"` RegisteredAtMs int64 `json:"registered_at_ms"` } // pendingRaceNameItem mirrors `ports.PendingRegistration` for the // self-service view. `source_game_id` is the game whose capable finish // promoted the reservation; `eligible_until_ms` is the deadline by // which `lobby.race_name.register` must succeed. type pendingRaceNameItem struct { CanonicalKey string `json:"canonical_key"` RaceName string `json:"race_name"` SourceGameID string `json:"source_game_id"` ReservedAtMs int64 `json:"reserved_at_ms"` EligibleUntilMs int64 `json:"eligible_until_ms"` } // raceNameReservationItem mirrors `ports.Reservation` enriched with // the current `game_status` joined from the game store. `game_status` // is empty when the underlying game record cannot be loaded. type raceNameReservationItem struct { CanonicalKey string `json:"canonical_key"` RaceName string `json:"race_name"` GameID string `json:"game_id"` ReservedAtMs int64 `json:"reserved_at_ms"` GameStatus string `json:"game_status"` } func (h *raceNameHandlers) handleListMy(writer http.ResponseWriter, request *http.Request) { if h.deps.ListMyRaceNames == nil { writeError(writer, http.StatusInternalServerError, "internal_error", "list my race names service is not wired") return } games := &gameHandlers{deps: h.deps, logger: h.logger} actor, ok := games.requireUserActor(writer, request) if !ok { return } out, err := h.deps.ListMyRaceNames.Handle(request.Context(), listmyracenames.Input{ Actor: actor, }) if err != nil { writeErrorFromService(writer, h.logger, err) return } resp := myRaceNamesResponse{ Registered: make([]registeredRaceNameItem, 0, len(out.Registered)), Pending: make([]pendingRaceNameItem, 0, len(out.Pending)), Reservations: make([]raceNameReservationItem, 0, len(out.Reservations)), } for _, entry := range out.Registered { resp.Registered = append(resp.Registered, registeredRaceNameItem{ CanonicalKey: entry.CanonicalKey, RaceName: entry.RaceName, SourceGameID: entry.SourceGameID, RegisteredAtMs: entry.RegisteredAtMs, }) } for _, entry := range out.Pending { resp.Pending = append(resp.Pending, pendingRaceNameItem{ CanonicalKey: entry.CanonicalKey, RaceName: entry.RaceName, SourceGameID: entry.SourceGameID, ReservedAtMs: entry.ReservedAtMs, EligibleUntilMs: entry.EligibleUntilMs, }) } for _, entry := range out.Reservations { resp.Reservations = append(resp.Reservations, raceNameReservationItem{ CanonicalKey: entry.CanonicalKey, RaceName: entry.RaceName, GameID: entry.GameID, ReservedAtMs: entry.ReservedAtMs, GameStatus: entry.GameStatus, }) } writeJSON(writer, http.StatusOK, resp) } func (h *raceNameHandlers) handleRegister(writer http.ResponseWriter, request *http.Request) { if h.deps.RegisterRaceName == nil { writeError(writer, http.StatusInternalServerError, "internal_error", "register race name service is not wired") return } games := &gameHandlers{deps: h.deps, logger: h.logger} actor, ok := games.requireUserActor(writer, request) if !ok { return } var body registerRaceNameRequest if err := decodeStrictJSON(request.Body, &body); err != nil { writeError(writer, http.StatusBadRequest, "invalid_request", err.Error()) return } out, err := h.deps.RegisterRaceName.Handle(request.Context(), registerracename.Input{ Actor: actor, SourceGameID: common.GameID(body.SourceGameID), RaceName: body.RaceName, }) if err != nil { writeErrorFromService(writer, h.logger, err) return } writeJSON(writer, http.StatusOK, registerRaceNameResponse{ CanonicalKey: out.CanonicalKey, RaceName: out.RaceName, SourceGameID: out.SourceGameID, RegisteredAtMs: out.RegisteredAtMs, }) }