feat: game lobby service
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
package internalhttp
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"galaxy/lobby/internal/domain/application"
|
||||
"galaxy/lobby/internal/domain/common"
|
||||
"galaxy/lobby/internal/domain/membership"
|
||||
"galaxy/lobby/internal/service/approveapplication"
|
||||
"galaxy/lobby/internal/service/rejectapplication"
|
||||
"galaxy/lobby/internal/service/shared"
|
||||
)
|
||||
|
||||
// Internal HTTP route patterns for the admin application
|
||||
// surface. Submit is intentionally not exposed on the internal port —
|
||||
// applicants are authenticated users, never Admin Service.
|
||||
const (
|
||||
approveApplicationPath = "/api/v1/lobby/games/{game_id}/applications/{application_id}/approve"
|
||||
rejectApplicationPath = "/api/v1/lobby/games/{game_id}/applications/{application_id}/reject"
|
||||
|
||||
applicationIDPathParamValue = "application_id"
|
||||
)
|
||||
|
||||
// applicationRecordResponse mirrors the OpenAPI ApplicationRecord schema
|
||||
// on the internal port.
|
||||
type applicationRecordResponse struct {
|
||||
ApplicationID string `json:"application_id"`
|
||||
GameID string `json:"game_id"`
|
||||
ApplicantUserID string `json:"applicant_user_id"`
|
||||
RaceName string `json:"race_name"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
DecidedAt *int64 `json:"decided_at,omitempty"`
|
||||
}
|
||||
|
||||
func encodeApplicationRecord(record application.Application) applicationRecordResponse {
|
||||
resp := applicationRecordResponse{
|
||||
ApplicationID: record.ApplicationID.String(),
|
||||
GameID: record.GameID.String(),
|
||||
ApplicantUserID: record.ApplicantUserID,
|
||||
RaceName: record.RaceName,
|
||||
Status: string(record.Status),
|
||||
CreatedAt: record.CreatedAt.UTC().UnixMilli(),
|
||||
}
|
||||
if record.DecidedAt != nil {
|
||||
decided := record.DecidedAt.UTC().UnixMilli()
|
||||
resp.DecidedAt = &decided
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
// membershipRecordResponse mirrors the OpenAPI MembershipRecord schema.
|
||||
// canonical_key is intentionally omitted from the wire shape.
|
||||
type membershipRecordResponse struct {
|
||||
MembershipID string `json:"membership_id"`
|
||||
GameID string `json:"game_id"`
|
||||
UserID string `json:"user_id"`
|
||||
RaceName string `json:"race_name"`
|
||||
Status string `json:"status"`
|
||||
JoinedAt int64 `json:"joined_at"`
|
||||
RemovedAt *int64 `json:"removed_at,omitempty"`
|
||||
}
|
||||
|
||||
func encodeMembershipRecord(record membership.Membership) membershipRecordResponse {
|
||||
resp := membershipRecordResponse{
|
||||
MembershipID: record.MembershipID.String(),
|
||||
GameID: record.GameID.String(),
|
||||
UserID: record.UserID,
|
||||
RaceName: record.RaceName,
|
||||
Status: string(record.Status),
|
||||
JoinedAt: record.JoinedAt.UTC().UnixMilli(),
|
||||
}
|
||||
if record.RemovedAt != nil {
|
||||
removed := record.RemovedAt.UTC().UnixMilli()
|
||||
resp.RemovedAt = &removed
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
func registerApplicationRoutes(mux *http.ServeMux, deps Dependencies, logger *slog.Logger) {
|
||||
h := &applicationHandlers{
|
||||
deps: deps,
|
||||
logger: logger.With("component", "internal_http.applications"),
|
||||
}
|
||||
mux.HandleFunc("POST "+approveApplicationPath, h.handleApprove)
|
||||
mux.HandleFunc("POST "+rejectApplicationPath, h.handleReject)
|
||||
}
|
||||
|
||||
type applicationHandlers struct {
|
||||
deps Dependencies
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func (h *applicationHandlers) extractGameID(writer http.ResponseWriter, request *http.Request) (common.GameID, bool) {
|
||||
raw := request.PathValue(gameIDPathParamValue)
|
||||
if strings.TrimSpace(raw) == "" {
|
||||
writeError(writer, http.StatusBadRequest, "invalid_request", "game id is required")
|
||||
return "", false
|
||||
}
|
||||
return common.GameID(raw), true
|
||||
}
|
||||
|
||||
func (h *applicationHandlers) extractApplicationID(writer http.ResponseWriter, request *http.Request) (common.ApplicationID, bool) {
|
||||
raw := request.PathValue(applicationIDPathParamValue)
|
||||
if strings.TrimSpace(raw) == "" {
|
||||
writeError(writer, http.StatusBadRequest, "invalid_request", "application id is required")
|
||||
return "", false
|
||||
}
|
||||
return common.ApplicationID(raw), true
|
||||
}
|
||||
|
||||
func (h *applicationHandlers) handleApprove(writer http.ResponseWriter, request *http.Request) {
|
||||
if h.deps.ApproveApplication == nil {
|
||||
writeError(writer, http.StatusInternalServerError, "internal_error", "approve application service is not wired")
|
||||
return
|
||||
}
|
||||
gameID, ok := h.extractGameID(writer, request)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
applicationID, ok := h.extractApplicationID(writer, request)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
record, err := h.deps.ApproveApplication.Handle(request.Context(), approveapplication.Input{
|
||||
Actor: shared.NewAdminActor(),
|
||||
GameID: gameID,
|
||||
ApplicationID: applicationID,
|
||||
})
|
||||
if err != nil {
|
||||
writeErrorFromService(writer, h.logger, err)
|
||||
return
|
||||
}
|
||||
writeJSON(writer, http.StatusOK, encodeMembershipRecord(record))
|
||||
}
|
||||
|
||||
func (h *applicationHandlers) handleReject(writer http.ResponseWriter, request *http.Request) {
|
||||
if h.deps.RejectApplication == nil {
|
||||
writeError(writer, http.StatusInternalServerError, "internal_error", "reject application service is not wired")
|
||||
return
|
||||
}
|
||||
gameID, ok := h.extractGameID(writer, request)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
applicationID, ok := h.extractApplicationID(writer, request)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
record, err := h.deps.RejectApplication.Handle(request.Context(), rejectapplication.Input{
|
||||
Actor: shared.NewAdminActor(),
|
||||
GameID: gameID,
|
||||
ApplicationID: applicationID,
|
||||
})
|
||||
if err != nil {
|
||||
writeErrorFromService(writer, h.logger, err)
|
||||
return
|
||||
}
|
||||
writeJSON(writer, http.StatusOK, encodeApplicationRecord(record))
|
||||
}
|
||||
Reference in New Issue
Block a user