2820823b36
Add an optional API-key layer to the public read-only API. Keys grant higher per-minute rate limits and attribute usage; anonymous callers are still allowed at a lower IP-based budget. - migration 0008_api_key: api_key table (sha256 hash only, plaintext shown once) - apikey pkg: key generation + hashing - ratelimit pkg: Redis fixed-window limiter + per-key usage counters; fails open - public API middleware: X-API-Key / Bearer auth, X-RateLimit-* headers, 429+Retry-After - admin: issue/list/revoke keys + usage view (API + UI tab) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
71 lines
2.1 KiB
Go
71 lines
2.1 KiB
Go
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 != "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"})
|
|
}
|