244 lines
7.2 KiB
Go
244 lines
7.2 KiB
Go
package publichttp
|
|
|
|
import (
|
|
"log/slog"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"galaxy/lobby/internal/domain/common"
|
|
"galaxy/lobby/internal/domain/invite"
|
|
"galaxy/lobby/internal/service/createinvite"
|
|
"galaxy/lobby/internal/service/declineinvite"
|
|
"galaxy/lobby/internal/service/redeeminvite"
|
|
"galaxy/lobby/internal/service/revokeinvite"
|
|
"galaxy/lobby/internal/service/shared"
|
|
)
|
|
|
|
// Public HTTP route patterns for the invite surface.
|
|
const (
|
|
createInvitePath = "/api/v1/lobby/games/{game_id}/invites"
|
|
redeemInvitePath = "/api/v1/lobby/games/{game_id}/invites/{invite_id}/redeem"
|
|
declineInvitePath = "/api/v1/lobby/games/{game_id}/invites/{invite_id}/decline"
|
|
revokeInvitePath = "/api/v1/lobby/games/{game_id}/invites/{invite_id}/revoke"
|
|
|
|
inviteIDPathParamValue = "invite_id"
|
|
)
|
|
|
|
// createInviteRequest is the JSON shape for
|
|
// `POST /api/v1/lobby/games/{game_id}/invites`.
|
|
type createInviteRequest struct {
|
|
InviteeUserID string `json:"invitee_user_id"`
|
|
}
|
|
|
|
// redeemInviteRequest is the JSON shape for
|
|
// `POST /api/v1/lobby/games/{game_id}/invites/{invite_id}/redeem`.
|
|
type redeemInviteRequest struct {
|
|
RaceName string `json:"race_name"`
|
|
}
|
|
|
|
// inviteRecordResponse mirrors the OpenAPI InviteRecord schema. RaceName is
|
|
// omitted from the wire shape until the invite transitions to redeemed.
|
|
type inviteRecordResponse struct {
|
|
InviteID string `json:"invite_id"`
|
|
GameID string `json:"game_id"`
|
|
InviterUserID string `json:"inviter_user_id"`
|
|
InviteeUserID string `json:"invitee_user_id"`
|
|
RaceName string `json:"race_name,omitempty"`
|
|
Status string `json:"status"`
|
|
CreatedAt int64 `json:"created_at"`
|
|
ExpiresAt int64 `json:"expires_at"`
|
|
DecidedAt *int64 `json:"decided_at,omitempty"`
|
|
}
|
|
|
|
func encodeInviteRecord(record invite.Invite) inviteRecordResponse {
|
|
resp := inviteRecordResponse{
|
|
InviteID: record.InviteID.String(),
|
|
GameID: record.GameID.String(),
|
|
InviterUserID: record.InviterUserID,
|
|
InviteeUserID: record.InviteeUserID,
|
|
RaceName: record.RaceName,
|
|
Status: string(record.Status),
|
|
CreatedAt: record.CreatedAt.UTC().UnixMilli(),
|
|
ExpiresAt: record.ExpiresAt.UTC().UnixMilli(),
|
|
}
|
|
if record.DecidedAt != nil {
|
|
decided := record.DecidedAt.UTC().UnixMilli()
|
|
resp.DecidedAt = &decided
|
|
}
|
|
return resp
|
|
}
|
|
|
|
// registerInviteRoutes binds the four invite routes.
|
|
func registerInviteRoutes(mux *http.ServeMux, deps Dependencies, logger *slog.Logger) {
|
|
h := &inviteHandlers{
|
|
deps: deps,
|
|
logger: logger.With("component", "public_http.invites"),
|
|
}
|
|
mux.HandleFunc("POST "+createInvitePath, h.handleCreate)
|
|
mux.HandleFunc("POST "+redeemInvitePath, h.handleRedeem)
|
|
mux.HandleFunc("POST "+declineInvitePath, h.handleDecline)
|
|
mux.HandleFunc("POST "+revokeInvitePath, h.handleRevoke)
|
|
}
|
|
|
|
type inviteHandlers struct {
|
|
deps Dependencies
|
|
logger *slog.Logger
|
|
}
|
|
|
|
func (h *inviteHandlers) 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 *inviteHandlers) extractInviteID(writer http.ResponseWriter, request *http.Request) (common.InviteID, bool) {
|
|
raw := request.PathValue(inviteIDPathParamValue)
|
|
if strings.TrimSpace(raw) == "" {
|
|
writeError(writer, http.StatusBadRequest, "invalid_request", "invite id is required")
|
|
return "", false
|
|
}
|
|
return common.InviteID(raw), true
|
|
}
|
|
|
|
func (h *inviteHandlers) 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 *inviteHandlers) handleCreate(writer http.ResponseWriter, request *http.Request) {
|
|
if h.deps.CreateInvite == nil {
|
|
writeError(writer, http.StatusInternalServerError, "internal_error", "create invite 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 createInviteRequest
|
|
if err := decodeStrictJSON(request.Body, &body); err != nil {
|
|
writeError(writer, http.StatusBadRequest, "invalid_request", err.Error())
|
|
return
|
|
}
|
|
|
|
record, err := h.deps.CreateInvite.Handle(request.Context(), createinvite.Input{
|
|
Actor: shared.NewUserActor(userID),
|
|
GameID: gameID,
|
|
InviteeUserID: body.InviteeUserID,
|
|
})
|
|
if err != nil {
|
|
writeErrorFromService(writer, h.logger, err)
|
|
return
|
|
}
|
|
writeJSON(writer, http.StatusCreated, encodeInviteRecord(record))
|
|
}
|
|
|
|
func (h *inviteHandlers) handleRedeem(writer http.ResponseWriter, request *http.Request) {
|
|
if h.deps.RedeemInvite == nil {
|
|
writeError(writer, http.StatusInternalServerError, "internal_error", "redeem invite service is not wired")
|
|
return
|
|
}
|
|
userID, ok := h.requireUserActor(writer, request)
|
|
if !ok {
|
|
return
|
|
}
|
|
gameID, ok := h.extractGameID(writer, request)
|
|
if !ok {
|
|
return
|
|
}
|
|
inviteID, ok := h.extractInviteID(writer, request)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
var body redeemInviteRequest
|
|
if err := decodeStrictJSON(request.Body, &body); err != nil {
|
|
writeError(writer, http.StatusBadRequest, "invalid_request", err.Error())
|
|
return
|
|
}
|
|
|
|
record, err := h.deps.RedeemInvite.Handle(request.Context(), redeeminvite.Input{
|
|
Actor: shared.NewUserActor(userID),
|
|
GameID: gameID,
|
|
InviteID: inviteID,
|
|
RaceName: body.RaceName,
|
|
})
|
|
if err != nil {
|
|
writeErrorFromService(writer, h.logger, err)
|
|
return
|
|
}
|
|
writeJSON(writer, http.StatusOK, encodeMembershipRecord(record))
|
|
}
|
|
|
|
func (h *inviteHandlers) handleDecline(writer http.ResponseWriter, request *http.Request) {
|
|
if h.deps.DeclineInvite == nil {
|
|
writeError(writer, http.StatusInternalServerError, "internal_error", "decline invite service is not wired")
|
|
return
|
|
}
|
|
userID, ok := h.requireUserActor(writer, request)
|
|
if !ok {
|
|
return
|
|
}
|
|
gameID, ok := h.extractGameID(writer, request)
|
|
if !ok {
|
|
return
|
|
}
|
|
inviteID, ok := h.extractInviteID(writer, request)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
record, err := h.deps.DeclineInvite.Handle(request.Context(), declineinvite.Input{
|
|
Actor: shared.NewUserActor(userID),
|
|
GameID: gameID,
|
|
InviteID: inviteID,
|
|
})
|
|
if err != nil {
|
|
writeErrorFromService(writer, h.logger, err)
|
|
return
|
|
}
|
|
writeJSON(writer, http.StatusOK, encodeInviteRecord(record))
|
|
}
|
|
|
|
func (h *inviteHandlers) handleRevoke(writer http.ResponseWriter, request *http.Request) {
|
|
if h.deps.RevokeInvite == nil {
|
|
writeError(writer, http.StatusInternalServerError, "internal_error", "revoke invite service is not wired")
|
|
return
|
|
}
|
|
userID, ok := h.requireUserActor(writer, request)
|
|
if !ok {
|
|
return
|
|
}
|
|
gameID, ok := h.extractGameID(writer, request)
|
|
if !ok {
|
|
return
|
|
}
|
|
inviteID, ok := h.extractInviteID(writer, request)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
record, err := h.deps.RevokeInvite.Handle(request.Context(), revokeinvite.Input{
|
|
Actor: shared.NewUserActor(userID),
|
|
GameID: gameID,
|
|
InviteID: inviteID,
|
|
})
|
|
if err != nil {
|
|
writeErrorFromService(writer, h.logger, err)
|
|
return
|
|
}
|
|
writeJSON(writer, http.StatusOK, encodeInviteRecord(record))
|
|
}
|