Files
ez-api/internal/api/api_key_handler.go
zenfun 33838b1e2c feat(api): wrap JSON responses in envelope
Add response envelope middleware to standardize JSON responses as
`{code,data,message}` with consistent business codes across endpoints.
Update Swagger annotations and tests to reflect the new response shape.

BREAKING CHANGE: API responses are now wrapped in a response envelope; clients must read payloads from `data` and handle `code`/`message` fields.
2026-01-10 00:15:08 +08:00

309 lines
9.8 KiB
Go

package api
import (
"net/http"
"strings"
"github.com/ez-api/ez-api/internal/dto"
"github.com/ez-api/ez-api/internal/model"
"github.com/gin-gonic/gin"
)
// CreateAPIKey godoc
// @Summary Create an API key
// @Description Create an API key for a provider group
// @Tags admin
// @Accept json
// @Produce json
// @Security AdminAuth
// @Param key body dto.APIKeyDTO true "API key payload"
// @Success 201 {object} ResponseEnvelope{data=model.APIKey}
// @Failure 400 {object} ResponseEnvelope{data=gin.H}
// @Failure 500 {object} ResponseEnvelope{data=gin.H}
// @Router /admin/api-keys [post]
func (h *Handler) CreateAPIKey(c *gin.Context) {
var req dto.APIKeyDTO
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if req.GroupID == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "group_id required"})
return
}
var group model.ProviderGroup
if err := h.db.First(&group, req.GroupID).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "provider group not found"})
return
}
apiKey := strings.TrimSpace(req.APIKey)
status := strings.TrimSpace(req.Status)
if status == "" {
status = "active"
}
autoBan := true
if req.AutoBan != nil {
autoBan = *req.AutoBan
}
key := model.APIKey{
GroupID: req.GroupID,
APIKey: apiKey,
AccessToken: strings.TrimSpace(req.AccessToken),
RefreshToken: strings.TrimSpace(req.RefreshToken),
AccountID: strings.TrimSpace(req.AccountID),
ProjectID: strings.TrimSpace(req.ProjectID),
Weight: normalizeWeight(req.Weight),
Status: status,
AutoBan: autoBan,
BanReason: strings.TrimSpace(req.BanReason),
}
if !req.ExpiresAt.IsZero() {
tu := req.ExpiresAt.UTC()
key.ExpiresAt = &tu
}
if !req.BanUntil.IsZero() {
tu := req.BanUntil.UTC()
key.BanUntil = &tu
}
if err := h.groupManager.ValidateAPIKey(group, key); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := h.db.Create(&key).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create api key", "details": err.Error()})
return
}
if err := h.sync.SyncProvidersForAPIKey(h.db, key.ID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync providers", "details": err.Error()})
return
}
if err := h.sync.SyncBindingsForAPIKey(h.db, key.ID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync bindings", "details": err.Error()})
return
}
c.JSON(http.StatusCreated, key)
}
// ListAPIKeys godoc
// @Summary List API keys
// @Description List API keys with optional filters
// @Tags admin
// @Produce json
// @Security AdminAuth
// @Param page query int false "page (1-based)"
// @Param limit query int false "limit (default 50, max 200)"
// @Param group_id query int false "filter by group_id"
// @Param status query string false "filter by status (active, suspended, auto_disabled, manual_disabled)"
// @Success 200 {object} ResponseEnvelope{data=[]model.APIKey}
// @Failure 500 {object} ResponseEnvelope{data=gin.H}
// @Router /admin/api-keys [get]
func (h *Handler) ListAPIKeys(c *gin.Context) {
var keys []model.APIKey
q := h.db.Model(&model.APIKey{}).Order("id desc")
if groupID := strings.TrimSpace(c.Query("group_id")); groupID != "" {
q = q.Where("group_id = ?", groupID)
}
if status := strings.TrimSpace(c.Query("status")); status != "" {
q = q.Where("status = ?", status)
}
query := parseListQuery(c)
q = applyListPagination(q, query)
if err := q.Find(&keys).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list api keys", "details": err.Error()})
return
}
c.JSON(http.StatusOK, keys)
}
// GetAPIKey godoc
// @Summary Get API key
// @Description Get an API key by id
// @Tags admin
// @Produce json
// @Security AdminAuth
// @Param id path int true "APIKey ID"
// @Success 200 {object} ResponseEnvelope{data=model.APIKey}
// @Failure 400 {object} ResponseEnvelope{data=gin.H}
// @Failure 404 {object} ResponseEnvelope{data=gin.H}
// @Failure 500 {object} ResponseEnvelope{data=gin.H}
// @Router /admin/api-keys/{id} [get]
func (h *Handler) GetAPIKey(c *gin.Context) {
id, ok := parseUintParam(c, "id")
if !ok {
return
}
var key model.APIKey
if err := h.db.First(&key, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "api key not found"})
return
}
c.JSON(http.StatusOK, key)
}
// UpdateAPIKey godoc
// @Summary Update API key
// @Description Update an API key
// @Tags admin
// @Accept json
// @Produce json
// @Security AdminAuth
// @Param id path int true "APIKey ID"
// @Param key body dto.APIKeyDTO true "API key payload"
// @Success 200 {object} ResponseEnvelope{data=model.APIKey}
// @Failure 400 {object} ResponseEnvelope{data=gin.H}
// @Failure 404 {object} ResponseEnvelope{data=gin.H}
// @Failure 500 {object} ResponseEnvelope{data=gin.H}
// @Router /admin/api-keys/{id} [put]
func (h *Handler) UpdateAPIKey(c *gin.Context) {
id, ok := parseUintParam(c, "id")
if !ok {
return
}
var key model.APIKey
if err := h.db.First(&key, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "api key not found"})
return
}
var req dto.APIKeyDTO
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
update := map[string]any{}
groupID := key.GroupID
if req.GroupID != 0 {
groupID = req.GroupID
}
var group model.ProviderGroup
if err := h.db.First(&group, groupID).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "provider group not found"})
return
}
if req.GroupID != 0 {
update["group_id"] = req.GroupID
}
if strings.TrimSpace(req.APIKey) != "" {
update["api_key"] = strings.TrimSpace(req.APIKey)
}
if strings.TrimSpace(req.AccessToken) != "" {
update["access_token"] = strings.TrimSpace(req.AccessToken)
}
if strings.TrimSpace(req.RefreshToken) != "" {
update["refresh_token"] = strings.TrimSpace(req.RefreshToken)
}
if !req.ExpiresAt.IsZero() {
tu := req.ExpiresAt.UTC()
update["expires_at"] = &tu
}
if strings.TrimSpace(req.AccountID) != "" {
update["account_id"] = strings.TrimSpace(req.AccountID)
}
if strings.TrimSpace(req.ProjectID) != "" {
update["project_id"] = strings.TrimSpace(req.ProjectID)
}
if req.Weight > 0 {
update["weight"] = normalizeWeight(req.Weight)
}
if strings.TrimSpace(req.Status) != "" {
update["status"] = strings.TrimSpace(req.Status)
}
if req.AutoBan != nil {
update["auto_ban"] = *req.AutoBan
}
if req.BanReason != "" || strings.TrimSpace(req.Status) == "active" {
update["ban_reason"] = strings.TrimSpace(req.BanReason)
}
if !req.BanUntil.IsZero() {
tu := req.BanUntil.UTC()
update["ban_until"] = &tu
}
if req.BanUntil.IsZero() && strings.TrimSpace(req.Status) == "active" {
update["ban_until"] = nil
}
if req.GroupID != 0 || strings.TrimSpace(req.APIKey) != "" || strings.TrimSpace(req.AccessToken) != "" || strings.TrimSpace(req.RefreshToken) != "" {
nextKey := key
if v, ok := update["api_key"].(string); ok {
nextKey.APIKey = v
}
if v, ok := update["access_token"].(string); ok {
nextKey.AccessToken = v
}
if v, ok := update["refresh_token"].(string); ok {
nextKey.RefreshToken = v
}
if req.GroupID != 0 {
nextKey.GroupID = req.GroupID
}
if err := h.groupManager.ValidateAPIKey(group, nextKey); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
}
if err := h.db.Model(&key).Updates(update).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update api key", "details": err.Error()})
return
}
if err := h.db.First(&key, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to reload api key", "details": err.Error()})
return
}
if err := h.sync.SyncProvidersForAPIKey(h.db, key.ID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync providers", "details": err.Error()})
return
}
if err := h.sync.SyncBindingsForAPIKey(h.db, key.ID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync bindings", "details": err.Error()})
return
}
c.JSON(http.StatusOK, key)
}
// DeleteAPIKey godoc
// @Summary Delete API key
// @Description Delete an API key
// @Tags admin
// @Produce json
// @Security AdminAuth
// @Param id path int true "APIKey ID"
// @Success 200 {object} ResponseEnvelope{data=gin.H}
// @Failure 400 {object} ResponseEnvelope{data=gin.H}
// @Failure 404 {object} ResponseEnvelope{data=gin.H}
// @Failure 500 {object} ResponseEnvelope{data=gin.H}
// @Router /admin/api-keys/{id} [delete]
func (h *Handler) DeleteAPIKey(c *gin.Context) {
id, ok := parseUintParam(c, "id")
if !ok {
return
}
var key model.APIKey
if err := h.db.First(&key, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "api key not found"})
return
}
if err := h.db.Delete(&key).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete api key", "details": err.Error()})
return
}
if err := h.sync.SyncProvidersForAPIKey(h.db, key.ID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync providers", "details": err.Error()})
return
}
if err := h.sync.SyncBindingsForAPIKey(h.db, key.ID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync bindings", "details": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"status": "deleted"})
}