package publichttp import ( "log/slog" "net/http" "strings" "galaxy/lobby/internal/domain/common" "galaxy/lobby/internal/domain/membership" "galaxy/lobby/internal/service/blockmember" "galaxy/lobby/internal/service/listmemberships" "galaxy/lobby/internal/service/removemember" ) // Public HTTP route patterns for the membership routes. const ( listMembershipsPath = "/api/v1/lobby/games/{game_id}/memberships" removeMemberPath = "/api/v1/lobby/games/{game_id}/memberships/{membership_id}/remove" blockMemberPath = "/api/v1/lobby/games/{game_id}/memberships/{membership_id}/block" membershipIDPathParamValue = "membership_id" ) // registerMembershipRoutes binds the membership // routes on the public port. The X-User-ID header is required on every // route; admins use the internal port equivalents. func registerMembershipRoutes(mux *http.ServeMux, deps Dependencies, logger *slog.Logger) { h := &membershipHandlers{ deps: deps, logger: logger.With("component", "public_http.memberships"), } mux.HandleFunc("GET "+listMembershipsPath, h.handleList) mux.HandleFunc("POST "+removeMemberPath, h.handleRemove) mux.HandleFunc("POST "+blockMemberPath, h.handleBlock) } type membershipHandlers struct { deps Dependencies logger *slog.Logger } func (h *membershipHandlers) extractMembershipID(writer http.ResponseWriter, request *http.Request) (common.MembershipID, bool) { raw := request.PathValue(membershipIDPathParamValue) if strings.TrimSpace(raw) == "" { writeError(writer, http.StatusBadRequest, "invalid_request", "membership id is required") return "", false } return common.MembershipID(raw), true } func (h *membershipHandlers) handleRemove(writer http.ResponseWriter, request *http.Request) { if h.deps.RemoveMember == nil { writeError(writer, http.StatusInternalServerError, "internal_error", "remove member service is not wired") return } games := &gameHandlers{deps: h.deps, logger: h.logger} actor, ok := games.requireUserActor(writer, request) if !ok { return } gameID, ok := games.extractGameID(writer, request) if !ok { return } membershipID, ok := h.extractMembershipID(writer, request) if !ok { return } record, err := h.deps.RemoveMember.Handle(request.Context(), removemember.Input{ Actor: actor, GameID: gameID, MembershipID: membershipID, }) if err != nil { writeErrorFromService(writer, h.logger, err) return } writeJSON(writer, http.StatusOK, encodeMembershipRecord(record)) } // membershipListResponse mirrors the OpenAPI MembershipListResponse // schema. Items are always non-nil so the JSON form carries `[]` rather // than `null` for empty pages. type membershipListResponse struct { Items []membershipRecordResponse `json:"items"` NextPageToken string `json:"next_page_token,omitempty"` } func encodeMembershipList(items []membership.Membership, nextPageToken string) membershipListResponse { resp := membershipListResponse{ Items: make([]membershipRecordResponse, 0, len(items)), NextPageToken: nextPageToken, } for _, item := range items { resp.Items = append(resp.Items, encodeMembershipRecord(item)) } return resp } func (h *membershipHandlers) handleList(writer http.ResponseWriter, request *http.Request) { if h.deps.ListMemberships == nil { writeError(writer, http.StatusInternalServerError, "internal_error", "list memberships service is not wired") return } games := &gameHandlers{deps: h.deps, logger: h.logger} actor, ok := games.requireUserActor(writer, request) if !ok { return } gameID, ok := games.extractGameID(writer, request) if !ok { return } page, ok := parsePage(writer, request) if !ok { return } out, err := h.deps.ListMemberships.Handle(request.Context(), listmemberships.Input{ Actor: actor, GameID: gameID, Page: page, }) if err != nil { writeErrorFromService(writer, h.logger, err) return } writeJSON(writer, http.StatusOK, encodeMembershipList(out.Items, out.NextPageToken)) } func (h *membershipHandlers) handleBlock(writer http.ResponseWriter, request *http.Request) { if h.deps.BlockMember == nil { writeError(writer, http.StatusInternalServerError, "internal_error", "block member service is not wired") return } games := &gameHandlers{deps: h.deps, logger: h.logger} actor, ok := games.requireUserActor(writer, request) if !ok { return } gameID, ok := games.extractGameID(writer, request) if !ok { return } membershipID, ok := h.extractMembershipID(writer, request) if !ok { return } record, err := h.deps.BlockMember.Handle(request.Context(), blockmember.Input{ Actor: actor, GameID: gameID, MembershipID: membershipID, }) if err != nil { writeErrorFromService(writer, h.logger, err) return } writeJSON(writer, http.StatusOK, encodeMembershipRecord(record)) }