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)) }