// adminhandlers.go:admin 面(同端口,X-Service-Token == BROWSER_SERVER_ADMIN_TOKEN)。 // POST /admin/keys(签发,明文只回一次)、DELETE /admin/keys/{id}(吊销即时)、 // GET /admin/keys(列表只给 prefix/status 不给 hash)。 package gateway import ( "encoding/json" "net/http" "strconv" "strings" "time" ) // adminIssueReq 签发请求。 type adminIssueReq struct { ConsumerName string `json:"consumer_name"` Contact string `json:"contact,omitempty"` Scopes []string `json:"scopes,omitempty"` RPM int `json:"rpm,omitempty"` DailyQuota int `json:"daily,omitempty"` MonthlyQuota int `json:"monthly,omitempty"` ConcurrentSessions int `json:"concurrent,omitempty"` ExpiresAt string `json:"expires_at,omitempty"` // RFC3339,可空 } // adminIssueResp 签发响应(明文只回一次)。 type adminIssueResp struct { OK bool `json:"ok"` KeyID int64 `json:"key_id"` ConsumerID int64 `json:"consumer_id"` Key string `json:"key"` // 明文仅此一次 Prefix string `json:"prefix"` Note string `json:"note,omitempty"` // 配额口径声明(ITER-3 DECL-1) } // handleAdminIssue POST /admin/keys。 func (s *Server) handleAdminIssue(w http.ResponseWriter, r *http.Request) { var req adminIssueReq if err := jsonDecode(r, &req); err != nil { s.adminWriteErr(w, "请求体非法 JSON: "+err.Error(), http.StatusBadRequest) return } req.ConsumerName = strings.TrimSpace(req.ConsumerName) if req.ConsumerName == "" { s.adminWriteErr(w, "consumer_name 必填", http.StatusBadRequest) return } if req.RPM <= 0 { req.RPM = 60 } if req.DailyQuota <= 0 { req.DailyQuota = 1000 } if req.MonthlyQuota <= 0 { req.MonthlyQuota = 20000 } if req.ConcurrentSessions <= 0 { req.ConcurrentSessions = 2 } var expires *time.Time if req.ExpiresAt != "" { t, err := time.Parse(time.RFC3339, req.ExpiresAt) if err != nil { s.adminWriteErr(w, "expires_at 须为 RFC3339", http.StatusBadRequest) return } expires = &t } // 消费者主体:不存在即建(幂等名) cid, err := s.ensureConsumer(req.ConsumerName, req.Contact) if err != nil { s.adminWriteErr(w, err.Error(), http.StatusInternalServerError) return } plaintext, keyID, err := s.deps.Verifier.Issue(cid, req.ConsumerName, req.Scopes, req.RPM, req.DailyQuota, req.MonthlyQuota, req.ConcurrentSessions, expires) if err != nil { s.adminWriteErr(w, "签发失败: "+err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) _ = json.NewEncoder(w).Encode(adminIssueResp{ OK: true, KeyID: keyID, ConsumerID: cid, Key: plaintext, Prefix: plaintext[:10], // ITER-3 DECL-1:月配额首版未生效(仅日窗),响应显式声明口径。 Note: "monthly_quota 首版未生效(仅日窗)", }) } // handleAdminRevoke DELETE /admin/keys/{id}(吊销即时)。 func (s *Server) handleAdminRevoke(w http.ResponseWriter, r *http.Request) { idStr := r.PathValue("id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { s.adminWriteErr(w, "key id 非法", http.StatusBadRequest) return } if err := s.deps.DB.SetKeyStatus(id, "revoked"); err != nil { if err == errNotFound() { s.adminWriteErr(w, "key 不存在", http.StatusNotFound) return } s.adminWriteErr(w, "吊销失败: "+err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]any{"ok": true, "key_id": id, "status": "revoked"}) } // adminKeyItem 列表项(只给 prefix/status,不给 hash/salt)。 type adminKeyItem struct { ID int64 `json:"id"` ConsumerID int64 `json:"consumer_id"` Prefix string `json:"prefix"` Name string `json:"name"` Scopes string `json:"scopes"` RPM int `json:"rpm"` Daily int `json:"daily_quota"` Monthly int `json:"monthly_quota"` Status string `json:"status"` CreatedAt string `json:"created_at"` } // handleAdminList GET /admin/keys。 func (s *Server) handleAdminList(w http.ResponseWriter, _ *http.Request) { keys, err := s.deps.Glue.KeysAll() if err != nil { s.adminWriteErr(w, err.Error(), http.StatusInternalServerError) return } items := make([]adminKeyItem, 0, len(keys)) for _, k := range keys { items = append(items, adminKeyItem{ ID: k.ID, ConsumerID: k.ConsumerID, Prefix: k.Prefix, Name: k.Name, Scopes: strings.Join(k.Scopes, ","), RPM: k.RPM, Daily: k.DailyQuota, Monthly: k.MonthlyQuota, Status: k.Status, CreatedAt: k.CreatedAt.Format(time.RFC3339), }) } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]any{"ok": true, "keys": items}) } // adminWriteErr admin 错误输出(简单 JSON 对象,非消费信封)。 func (s *Server) adminWriteErr(w http.ResponseWriter, msg string, status int) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(map[string]any{"ok": false, "error": msg}) } // errNotFound store.ErrNotFound 镜像(避免 adminhandlers 直引 store 类型判断冗长)。 func errNotFound() error { return storeErrNotFound } // ensureConsumer 按名取消费者 id,不存在即建。 func (s *Server) ensureConsumer(name, contact string) (int64, error) { if cid, err := s.deps.Glue.ConsumerByName(name); err == nil { return cid, nil } return s.deps.DB.CreateConsumer(name, contact) }