223 lines
6.9 KiB
Go
223 lines
6.9 KiB
Go
package publichttp
|
|
|
|
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"
|
|
"galaxy/lobby/internal/service/submitapplication"
|
|
)
|
|
|
|
// Public HTTP route patterns for the application surface.
|
|
const (
|
|
submitApplicationPath = "/api/v1/lobby/games/{game_id}/applications"
|
|
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"
|
|
)
|
|
|
|
// submitApplicationRequest is the JSON shape for
|
|
// `POST /api/v1/lobby/games/{game_id}/applications`.
|
|
type submitApplicationRequest struct {
|
|
RaceName string `json:"race_name"`
|
|
}
|
|
|
|
// applicationRecordResponse mirrors the OpenAPI ApplicationRecord schema.
|
|
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; it is a
|
|
// lobby-internal field per design.
|
|
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
|
|
}
|
|
|
|
// registerApplicationRoutes binds the three application routes.
|
|
func registerApplicationRoutes(mux *http.ServeMux, deps Dependencies, logger *slog.Logger) {
|
|
h := &applicationHandlers{
|
|
deps: deps,
|
|
logger: logger.With("component", "public_http.applications"),
|
|
}
|
|
mux.HandleFunc("POST "+submitApplicationPath, h.handleSubmit)
|
|
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) requireUserActor(writer http.ResponseWriter, request *http.Request) (string, bool) {
|
|
userID := strings.TrimSpace(request.Header.Get(xUserIDHeader))
|
|
if userID == "" {
|
|
writeError(writer, http.StatusBadRequest, "invalid_request",
|
|
"X-User-ID header is required")
|
|
return "", false
|
|
}
|
|
return userID, true
|
|
}
|
|
|
|
func (h *applicationHandlers) handleSubmit(writer http.ResponseWriter, request *http.Request) {
|
|
if h.deps.SubmitApplication == nil {
|
|
writeError(writer, http.StatusInternalServerError, "internal_error", "submit application service is not wired")
|
|
return
|
|
}
|
|
userID, ok := h.requireUserActor(writer, request)
|
|
if !ok {
|
|
return
|
|
}
|
|
gameID, ok := h.extractGameID(writer, request)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
var body submitApplicationRequest
|
|
if err := decodeStrictJSON(request.Body, &body); err != nil {
|
|
writeError(writer, http.StatusBadRequest, "invalid_request", err.Error())
|
|
return
|
|
}
|
|
|
|
record, err := h.deps.SubmitApplication.Handle(request.Context(), submitapplication.Input{
|
|
Actor: shared.NewUserActor(userID),
|
|
GameID: gameID,
|
|
RaceName: body.RaceName,
|
|
})
|
|
if err != nil {
|
|
writeErrorFromService(writer, h.logger, err)
|
|
return
|
|
}
|
|
writeJSON(writer, http.StatusCreated, encodeApplicationRecord(record))
|
|
}
|
|
|
|
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
|
|
}
|
|
userID, ok := h.requireUserActor(writer, request)
|
|
if !ok {
|
|
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.NewUserActor(userID),
|
|
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
|
|
}
|
|
userID, ok := h.requireUserActor(writer, request)
|
|
if !ok {
|
|
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.NewUserActor(userID),
|
|
GameID: gameID,
|
|
ApplicationID: applicationID,
|
|
})
|
|
if err != nil {
|
|
writeErrorFromService(writer, h.logger, err)
|
|
return
|
|
}
|
|
writeJSON(writer, http.StatusOK, encodeApplicationRecord(record))
|
|
}
|