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
267 lines
7.8 KiB
Go
267 lines
7.8 KiB
Go
package api
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/ez-api/ez-api/internal/dto"
|
|
"github.com/ez-api/ez-api/internal/model"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// CreateBinding godoc
|
|
// @Summary Create a new binding
|
|
// @Description Create a new (namespace, public_model) binding to a provider group and selector
|
|
// @Tags admin
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security AdminAuth
|
|
// @Param binding body dto.BindingDTO true "Binding Info"
|
|
// @Success 201 {object} model.Binding
|
|
// @Failure 400 {object} gin.H
|
|
// @Failure 500 {object} gin.H
|
|
// @Router /admin/bindings [post]
|
|
func (h *Handler) CreateBinding(c *gin.Context) {
|
|
var req dto.BindingDTO
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
ns := strings.TrimSpace(req.Namespace)
|
|
pm := strings.TrimSpace(req.PublicModel)
|
|
if ns == "" || pm == "" || req.GroupID == 0 {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "namespace, public_model, and group_id required"})
|
|
return
|
|
}
|
|
|
|
if err := h.ensureActiveGroup(req.GroupID); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
st := strings.TrimSpace(req.Status)
|
|
if st == "" {
|
|
st = "active"
|
|
}
|
|
|
|
selectorType := strings.TrimSpace(req.SelectorType)
|
|
if selectorType == "" {
|
|
selectorType = "exact"
|
|
}
|
|
|
|
b := model.Binding{
|
|
Namespace: ns,
|
|
PublicModel: pm,
|
|
GroupID: req.GroupID,
|
|
Weight: normalizeWeight(req.Weight),
|
|
SelectorType: selectorType,
|
|
SelectorValue: strings.TrimSpace(req.SelectorValue),
|
|
Status: st,
|
|
}
|
|
|
|
if err := h.db.Create(&b).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create binding", "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, b)
|
|
}
|
|
|
|
// ListBindings godoc
|
|
// @Summary List bindings
|
|
// @Description List all configured bindings
|
|
// @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 search query string false "search by namespace/public_model"
|
|
// @Success 200 {array} model.Binding
|
|
// @Failure 500 {object} gin.H
|
|
// @Router /admin/bindings [get]
|
|
func (h *Handler) ListBindings(c *gin.Context) {
|
|
var out []model.Binding
|
|
q := h.db.Model(&model.Binding{}).Order("id desc")
|
|
query := parseListQuery(c)
|
|
q = applyListSearch(q, query.Search, "namespace", "public_model")
|
|
q = applyListPagination(q, query)
|
|
if err := q.Find(&out).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list bindings", "details": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, out)
|
|
}
|
|
|
|
// UpdateBinding godoc
|
|
// @Summary Update a binding
|
|
// @Description Update an existing binding
|
|
// @Tags admin
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security AdminAuth
|
|
// @Param id path int true "Binding ID"
|
|
// @Param binding body dto.BindingDTO true "Binding Info"
|
|
// @Success 200 {object} model.Binding
|
|
// @Failure 400 {object} gin.H
|
|
// @Failure 404 {object} gin.H
|
|
// @Failure 500 {object} gin.H
|
|
// @Router /admin/bindings/{id} [put]
|
|
func (h *Handler) UpdateBinding(c *gin.Context) {
|
|
idParam := c.Param("id")
|
|
id, err := strconv.Atoi(idParam)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
|
return
|
|
}
|
|
|
|
var existing model.Binding
|
|
if err := h.db.First(&existing, id).Error; err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "binding not found"})
|
|
return
|
|
}
|
|
|
|
var req dto.BindingDTO
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
if ns := strings.TrimSpace(req.Namespace); ns != "" {
|
|
existing.Namespace = ns
|
|
}
|
|
if pm := strings.TrimSpace(req.PublicModel); pm != "" {
|
|
existing.PublicModel = pm
|
|
}
|
|
if req.GroupID != 0 {
|
|
if err := h.ensureActiveGroup(req.GroupID); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
existing.GroupID = req.GroupID
|
|
}
|
|
if req.Weight > 0 {
|
|
existing.Weight = normalizeWeight(req.Weight)
|
|
}
|
|
if st := strings.TrimSpace(req.Status); st != "" {
|
|
existing.Status = st
|
|
}
|
|
if t := strings.TrimSpace(req.SelectorType); t != "" {
|
|
existing.SelectorType = t
|
|
}
|
|
if req.SelectorValue != "" {
|
|
existing.SelectorValue = strings.TrimSpace(req.SelectorValue)
|
|
}
|
|
|
|
if err := h.db.Save(&existing).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update binding", "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, existing)
|
|
}
|
|
|
|
// GetBinding godoc
|
|
// @Summary Get a binding
|
|
// @Description Get a binding by id
|
|
// @Tags admin
|
|
// @Produce json
|
|
// @Security AdminAuth
|
|
// @Param id path int true "Binding ID"
|
|
// @Success 200 {object} model.Binding
|
|
// @Failure 400 {object} gin.H
|
|
// @Failure 404 {object} gin.H
|
|
// @Failure 500 {object} gin.H
|
|
// @Router /admin/bindings/{id} [get]
|
|
func (h *Handler) GetBinding(c *gin.Context) {
|
|
idParam := c.Param("id")
|
|
id, err := strconv.Atoi(idParam)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
|
return
|
|
}
|
|
var existing model.Binding
|
|
if err := h.db.First(&existing, id).Error; err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "binding not found"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, existing)
|
|
}
|
|
|
|
// DeleteBinding godoc
|
|
// @Summary Delete a binding
|
|
// @Description Delete a binding by id and rebuild bindings snapshot
|
|
// @Tags admin
|
|
// @Produce json
|
|
// @Security AdminAuth
|
|
// @Param id path int true "Binding ID"
|
|
// @Success 200 {object} gin.H
|
|
// @Failure 400 {object} gin.H
|
|
// @Failure 404 {object} gin.H
|
|
// @Failure 500 {object} gin.H
|
|
// @Router /admin/bindings/{id} [delete]
|
|
func (h *Handler) DeleteBinding(c *gin.Context) {
|
|
idParam := c.Param("id")
|
|
id, err := strconv.Atoi(idParam)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
|
return
|
|
}
|
|
|
|
var existing model.Binding
|
|
if err := h.db.First(&existing, id).Error; err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "binding not found"})
|
|
return
|
|
}
|
|
if err := h.db.Delete(&existing).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete binding", "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"})
|
|
}
|
|
|
|
func normalizeWeight(weight int) int {
|
|
if weight <= 0 {
|
|
return 1
|
|
}
|
|
return weight
|
|
}
|
|
|
|
func (h *Handler) ensureActiveGroup(groupID uint) error {
|
|
var group model.ProviderGroup
|
|
if err := h.db.First(&group, groupID).Error; err != nil {
|
|
return fmt.Errorf("provider group not found")
|
|
}
|
|
if strings.TrimSpace(group.Status) != "" && strings.TrimSpace(group.Status) != "active" {
|
|
return fmt.Errorf("provider group not active")
|
|
}
|
|
var count int64
|
|
if err := h.db.Model(&model.APIKey{}).
|
|
Where("group_id = ? AND status = ?", groupID, "active").
|
|
Count(&count).Error; err != nil {
|
|
return fmt.Errorf("failed to check api keys")
|
|
}
|
|
if count == 0 {
|
|
return fmt.Errorf("provider group has no active api keys")
|
|
}
|
|
return nil
|
|
}
|