Unverified Commit e9188682 authored by Lemon-miaow's avatar Lemon-miaow
Browse files

fix(api): mc_uuid 统一校验为 UUID,非法值返回 400 bad_mc_uuid 并按规范形式存储

parent 5b0686dc
Loading
Loading
Loading
Loading
+8 −0
Changes for docs/openapi.yaml: 8 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -1422,6 +1422,8 @@ paths:
                    additionalProperties:
                      type: string
                      enum: [retiring, start_failed, owner, wake, owner_only, allowlist]
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

@@ -1505,6 +1507,8 @@ paths:
                required: [linked]
                properties:
                  linked: { type: boolean }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

@@ -1637,6 +1641,8 @@ paths:
                required: [blacklisted]
                properties:
                  blacklisted: { type: boolean }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

@@ -4684,6 +4690,8 @@ paths:
                properties:
                  ok: { type: boolean, const: true }
                  mc_uuid: { type: string, format: uuid }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
+33 −8
Changes for internal/api/handlers_account.go: 33 added lines, 8 removed lines.
Original line number Diff line number Diff line
@@ -6,6 +6,8 @@ import (
	"net/http"
	"strings"
	"time"

	"github.com/google/uuid"
)

// Account-linking endpoints (spec §10). The flow is forced by the
@@ -51,6 +53,28 @@ func validAuthSource(s string) bool {
	return s == authSourceMojang || s == authSourceThirdParty
}

// errBadMCUUID answers an mc_uuid that is not a UUID. Every mc_uuid column is
// Postgres's uuid type, which refuses such text with 22P02, and that reached the
// caller as a 500.
var errBadMCUUID = newError(http.StatusBadRequest, "bad_mc_uuid",
	"mc_uuid must be a UUID, such as 069a79f4-44e9-4726-a5be-fca90e38aaf5")

// parseMCUUID reads an mc_uuid from a request: surrounding spaces trimmed, empty
// → 400 bad_request "mc_uuid is required", not a UUID → errBadMCUUID. It returns
// the canonical lowercase hyphenated form, the text Postgres gives back for the
// column, so what a handler stores, echoes and compares is one spelling.
func parseMCUUID(s string) (string, error) {
	s = strings.TrimSpace(s)
	if s == "" {
		return "", newError(http.StatusBadRequest, "bad_request", "mc_uuid is required")
	}
	id, err := uuid.Parse(s)
	if err != nil {
		return "", errBadMCUUID
	}
	return id.String(), nil
}

// deriveAuthSource infers the auth source from the UUID's version nibble when
// the minting backend omitted auth_source. Felis-nano rewrites every
// third-party profile to a name-based UUIDv3 under its namespace before it ever
@@ -104,8 +128,9 @@ func (a *API) handleCreateLinkCode(w http.ResponseWriter, r *http.Request) {
		writeError(w, r, err)
		return
	}
	if req.MCUUID == "" {
		writeError(w, r, newError(http.StatusBadRequest, "bad_request", "mc_uuid is required"))
	mcUUID, err := parseMCUUID(req.MCUUID)
	if err != nil {
		writeError(w, r, err)
		return
	}
	// Default an omitted source from the UUID's version nibble (v3 = felis-nano
@@ -114,7 +139,7 @@ func (a *API) handleCreateLinkCode(w http.ResponseWriter, r *http.Request) {
	// stored value the panel will later mislabel.
	authSource := req.AuthSource
	if authSource == "" {
		authSource = deriveAuthSource(req.MCUUID)
		authSource = deriveAuthSource(mcUUID)
	}
	if !validAuthSource(authSource) {
		writeError(w, r, newError(http.StatusBadRequest, "bad_request",
@@ -127,7 +152,7 @@ func (a *API) handleCreateLinkCode(w http.ResponseWriter, r *http.Request) {
		return
	}
	expiresAt := a.now().Add(linkCodeTTL)
	if err := a.Repo.CreateLinkCode(r.Context(), code, req.MCUUID, authSource, expiresAt); err != nil {
	if err := a.Repo.CreateLinkCode(r.Context(), code, mcUUID, authSource, expiresAt); err != nil {
		writeError(w, r, err)
		return
	}
@@ -171,12 +196,12 @@ func (a *API) handleCreateLinkCode(w http.ResponseWriter, r *http.Request) {
// handlers_player_reclaim.go keeps CODE-ONLY (reclaimed_by_user_id stays NULL on
// the verifiable path); this endpoint reports link completion only, not that choice.
func (a *API) handleLinkStatus(w http.ResponseWriter, r *http.Request) {
	mcUUID := r.PathValue("mc_uuid")
	if mcUUID == "" {
		writeError(w, r, newError(http.StatusBadRequest, "bad_request", "mc_uuid is required"))
	mcUUID, err := parseMCUUID(r.PathValue("mc_uuid"))
	if err != nil {
		writeError(w, r, err)
		return
	}
	_, err := a.Repo.UserByMCUUID(r.Context(), mcUUID)
	_, err = a.Repo.UserByMCUUID(r.Context(), mcUUID)
	switch {
	case errors.Is(err, ErrNotFound):
		// Not linked yet. For the poller this is simply "keep waiting": velocity
+3 −3
Changes for internal/api/handlers_account_migrate.go: 3 added lines, 3 removed lines.
Original line number Diff line number Diff line
@@ -111,9 +111,9 @@ func (a *API) handleMigrateStart(w http.ResponseWriter, r *http.Request) {
		writeError(w, r, err)
		return
	}
	mcUUID := strings.TrimSpace(req.MCUUID)
	if mcUUID == "" {
		writeError(w, r, newError(http.StatusBadRequest, "bad_request", "mc_uuid is required"))
	mcUUID, err := parseMCUUID(req.MCUUID)
	if err != nil {
		writeError(w, r, err)
		return
	}
	sourceUserID, err := a.Repo.UserByMCUUID(r.Context(), mcUUID)
+1 −1
Changes for internal/api/handlers_allowlist.go: 1 added line, 1 removed line.
Original line number Diff line number Diff line
@@ -66,7 +66,7 @@ func (a *API) handleAllowlistSetWake(w http.ResponseWriter, r *http.Request) {
	}
	id, err := uuid.Parse(r.PathValue("uuid"))
	if err != nil {
		writeError(w, r, newError(http.StatusBadRequest, "bad_request", "invalid Minecraft UUID"))
		writeError(w, r, errBadMCUUID)
		return
	}
	var req allowlistWakeRequest
+15 −12
Changes for internal/api/handlers_internal.go: 15 added lines, 12 removed lines.
Original line number Diff line number Diff line
@@ -83,11 +83,12 @@ func (a *API) handleJoinEvent(w http.ResponseWriter, r *http.Request) {
		writeError(w, r, err)
		return
	}
	if req.MCUUID == "" {
		writeError(w, r, newError(http.StatusBadRequest, "bad_request", "mc_uuid is required"))
	mcUUID, err := parseMCUUID(req.MCUUID)
	if err != nil {
		writeError(w, r, err)
		return
	}
	if err := a.Repo.RecordJoin(r.Context(), name, req.MCUUID); err != nil {
	if err := a.Repo.RecordJoin(r.Context(), name, mcUUID); err != nil {
		a.writeLookupError(w, r, err)
		return
	}
@@ -120,8 +121,9 @@ func (a *API) handleInternalWake(w http.ResponseWriter, r *http.Request) {
		writeError(w, r, err)
		return
	}
	if req.MCUUID == "" {
		writeError(w, r, newError(http.StatusBadRequest, "bad_request", "mc_uuid is required"))
	mcUUID, err := parseMCUUID(req.MCUUID)
	if err != nil {
		writeError(w, r, err)
		return
	}

@@ -148,7 +150,7 @@ func (a *API) handleInternalWake(w http.ResponseWriter, r *http.Request) {
		return
	}

	if err := a.authorizeWakeByUUID(r.Context(), req.MCUUID, info, rec); err != nil {
	if err := a.authorizeWakeByUUID(r.Context(), mcUUID, info, rec); err != nil {
		writeError(w, r, err)
		return
	}
@@ -235,8 +237,9 @@ func (a *API) handleInternalClaim(w http.ResponseWriter, r *http.Request) {
		writeError(w, r, err)
		return
	}
	if req.MCUUID == "" {
		writeError(w, r, newError(http.StatusBadRequest, "bad_request", "mc_uuid is required"))
	mcUUID, err := parseMCUUID(req.MCUUID)
	if err != nil {
		writeError(w, r, err)
		return
	}

@@ -245,7 +248,7 @@ func (a *API) handleInternalClaim(w http.ResponseWriter, r *http.Request) {
	// no separate IsLinked check (mirrors the external claim's order, link → quota
	// → write). ErrNotFound here is "claimer not linked" (412), never "server
	// missing" — that distinction is the claim call's, below.
	userID, err := a.Repo.UserByMCUUID(r.Context(), req.MCUUID)
	userID, err := a.Repo.UserByMCUUID(r.Context(), mcUUID)
	if err != nil {
		if errors.Is(err, ErrNotFound) {
			writeError(w, r, newError(http.StatusPreconditionFailed, "not_linked",
@@ -364,9 +367,9 @@ const (
// since a retry gets past those. Retiring comes first: nobody may start such a
// server, so it is the reason a stranger is shown too.
func (a *API) handleInternalMenuAccess(w http.ResponseWriter, r *http.Request) {
	mcUUID := r.PathValue("mc_uuid")
	if mcUUID == "" {
		writeError(w, r, newError(http.StatusBadRequest, "bad_request", "mc_uuid is required"))
	mcUUID, err := parseMCUUID(r.PathValue("mc_uuid"))
	if err != nil {
		writeError(w, r, err)
		return
	}
	infos, err := a.Cluster.ListServers(r.Context())
Loading