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 }