package adminhandler import ( "encoding/json" "net/http" "strings" "github.com/go-chi/chi/v5" "github.com/baicai2026-baicai/goods/api/internal/adminstore" "github.com/baicai2026-baicai/goods/api/internal/auth" "github.com/baicai2026-baicai/goods/api/internal/ratelimit" ) // apiKeyView is an issued key plus its usage counters. type apiKeyView struct { adminstore.APIKeyRow Usage ratelimit.UsageStat `json:"usage"` } // ListAPIKeys returns all issued keys with usage stats merged in. func (h *Handler) ListAPIKeys(w http.ResponseWriter, r *http.Request) { keys, err := h.store.ListAPIKeys(r.Context()) if h.handleErr(w, err) { return } views := make([]apiKeyView, 0, len(keys)) for _, k := range keys { v := apiKeyView{APIKeyRow: k} if h.usage != nil { v.Usage = h.usage.Usage(r.Context(), k.ID) } views = append(views, v) } writeJSON(w, http.StatusOK, map[string]any{"items": views}) } // CreateAPIKey issues a new key and returns its plaintext exactly once. func (h *Handler) CreateAPIKey(w http.ResponseWriter, r *http.Request) { var in adminstore.APIKeyInput if err := json.NewDecoder(r.Body).Decode(&in); err != nil { writeError(w, http.StatusBadRequest, "bad_request", "invalid body") return } if strings.TrimSpace(in.Name) == "" { writeError(w, http.StatusBadRequest, "bad_request", "名称不能为空") return } if in.Tier != "" && in.Tier != "free" && in.Tier != "registered" && in.Tier != "partner" && in.Tier != "internal" { writeError(w, http.StatusBadRequest, "bad_request", "tier 取值无效") return } plaintext, row, err := h.store.CreateAPIKey(r.Context(), in, auth.UserFrom(r.Context())) if h.handleErr(w, err) { return } writeJSON(w, http.StatusCreated, map[string]any{ "key": plaintext, "item": row, "warning": "请立即复制保存此密钥,它只显示这一次,无法再次查看。", }) } // RevokeAPIKey disables a key. Subsequent requests with it are rejected. func (h *Handler) RevokeAPIKey(w http.ResponseWriter, r *http.Request) { if err := h.store.RevokeAPIKey(r.Context(), chi.URLParam(r, "id")); h.handleErr(w, err) { return } writeJSON(w, http.StatusOK, map[string]string{"status": "revoked"}) }