mirror of
https://github.com/EZ-Api/ez-api.git
synced 2026-01-13 17:47:51 +00:00
Restructure the provider management system by separating the monolithic Provider model into two distinct entities: - ProviderGroup: defines shared upstream configuration (type, base_url, google settings, models, status) - APIKey: represents individual credentials within a group (api_key, weight, status, auto_ban, ban settings) This change also updates: - Binding model to reference GroupID instead of RouteGroup string - All CRUD handlers for the new provider-group and api-key endpoints - Sync service to rebuild provider snapshots from joined tables - Model registry to aggregate capabilities across group/key pairs - Access handler to validate namespace existence and subset constraints - Migration importer to handle the new schema structure - All related tests to use the new model relationships BREAKING CHANGE: Provider API endpoints replaced with /provider-groups and /api-keys endpoints; Binding.RouteGroup replaced with Binding.GroupID
260 lines
7.7 KiB
Go
260 lines
7.7 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/ez-api/foundation/provider"
|
|
"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} model.APIKey
|
|
// @Failure 400 {object} gin.H
|
|
// @Failure 500 {object} 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)
|
|
ptype := provider.NormalizeType(group.Type)
|
|
if provider.IsGoogleFamily(ptype) && !provider.IsVertexFamily(ptype) && apiKey == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "api_key required for gemini api providers"})
|
|
return
|
|
}
|
|
|
|
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,
|
|
Weight: normalizeWeight(req.Weight),
|
|
Status: status,
|
|
AutoBan: autoBan,
|
|
BanReason: strings.TrimSpace(req.BanReason),
|
|
}
|
|
if !req.BanUntil.IsZero() {
|
|
tu := req.BanUntil.UTC()
|
|
key.BanUntil = &tu
|
|
}
|
|
|
|
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.SyncProviders(h.db); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync providers", "details": err.Error()})
|
|
return
|
|
}
|
|
if err := h.sync.SyncBindings(h.db); 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
|
|
// @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"
|
|
// @Success 200 {array} model.APIKey
|
|
// @Failure 500 {object} 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)
|
|
}
|
|
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} model.APIKey
|
|
// @Failure 400 {object} gin.H
|
|
// @Failure 404 {object} gin.H
|
|
// @Failure 500 {object} 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} model.APIKey
|
|
// @Failure 400 {object} gin.H
|
|
// @Failure 404 {object} gin.H
|
|
// @Failure 500 {object} 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{}
|
|
if req.GroupID != 0 {
|
|
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
|
|
}
|
|
update["group_id"] = req.GroupID
|
|
}
|
|
if strings.TrimSpace(req.APIKey) != "" {
|
|
update["api_key"] = strings.TrimSpace(req.APIKey)
|
|
}
|
|
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 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.SyncProviders(h.db); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync providers", "details": err.Error()})
|
|
return
|
|
}
|
|
if err := h.sync.SyncBindings(h.db); 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} gin.H
|
|
// @Failure 400 {object} gin.H
|
|
// @Failure 404 {object} gin.H
|
|
// @Failure 500 {object} 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.SyncProviders(h.db); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync providers", "details": err.Error()})
|
|
return
|
|
}
|
|
if err := h.sync.SyncBindings(h.db); 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"})
|
|
}
|