mirror of
https://github.com/EZ-Api/ez-api.git
synced 2026-01-13 17:47:51 +00:00
feat(admin/master): provider+master CRUD, token mgmt, logs APIs
This commit is contained in:
182
internal/api/provider_admin_handler.go
Normal file
182
internal/api/provider_admin_handler.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ez-api/ez-api/internal/model"
|
||||
"github.com/ez-api/foundation/provider"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ListProviders godoc
|
||||
// @Summary List providers
|
||||
// @Description List all configured upstream providers
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Security AdminAuth
|
||||
// @Success 200 {array} model.Provider
|
||||
// @Failure 500 {object} gin.H
|
||||
// @Router /admin/providers [get]
|
||||
func (h *Handler) ListProviders(c *gin.Context) {
|
||||
var providers []model.Provider
|
||||
if err := h.db.Order("id desc").Find(&providers).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list providers", "details": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, providers)
|
||||
}
|
||||
|
||||
// GetProvider godoc
|
||||
// @Summary Get provider
|
||||
// @Description Get a provider by id
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Security AdminAuth
|
||||
// @Param id path int true "Provider ID"
|
||||
// @Success 200 {object} model.Provider
|
||||
// @Failure 400 {object} gin.H
|
||||
// @Failure 404 {object} gin.H
|
||||
// @Failure 500 {object} gin.H
|
||||
// @Router /admin/providers/{id} [get]
|
||||
func (h *Handler) GetProvider(c *gin.Context) {
|
||||
id, ok := parseUintParam(c, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var p model.Provider
|
||||
if err := h.db.First(&p, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "provider not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, p)
|
||||
}
|
||||
|
||||
// DeleteProvider godoc
|
||||
// @Summary Delete provider
|
||||
// @Description Deletes a provider and triggers a full snapshot sync to avoid stale routing
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Security AdminAuth
|
||||
// @Param id path int true "Provider ID"
|
||||
// @Success 200 {object} gin.H
|
||||
// @Failure 400 {object} gin.H
|
||||
// @Failure 404 {object} gin.H
|
||||
// @Failure 500 {object} gin.H
|
||||
// @Router /admin/providers/{id} [delete]
|
||||
func (h *Handler) DeleteProvider(c *gin.Context) {
|
||||
id, ok := parseUintParam(c, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var p model.Provider
|
||||
if err := h.db.First(&p, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "provider not found"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.db.Delete(&p).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete provider", "details": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Full sync is the simplest safe option because provider deletion needs to remove
|
||||
// stale entries in Redis snapshots (config:providers) and refresh bindings.
|
||||
if err := h.sync.SyncAll(h.db); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync snapshots", "details": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"status": "deleted"})
|
||||
}
|
||||
|
||||
type testProviderResponse struct {
|
||||
StatusCode int `json:"status_code"`
|
||||
OK bool `json:"ok"`
|
||||
URL string `json:"url"`
|
||||
Body string `json:"body,omitempty"`
|
||||
}
|
||||
|
||||
// TestProvider godoc
|
||||
// @Summary Test provider connectivity
|
||||
// @Description Performs a lightweight upstream request to verify the provider configuration
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Security AdminAuth
|
||||
// @Param id path int true "Provider ID"
|
||||
// @Success 200 {object} testProviderResponse
|
||||
// @Failure 400 {object} gin.H
|
||||
// @Failure 404 {object} gin.H
|
||||
// @Failure 500 {object} gin.H
|
||||
// @Router /admin/providers/{id}/test [post]
|
||||
func (h *Handler) TestProvider(c *gin.Context) {
|
||||
id, ok := parseUintParam(c, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var p model.Provider
|
||||
if err := h.db.First(&p, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "provider not found"})
|
||||
return
|
||||
}
|
||||
|
||||
pt := provider.NormalizeType(p.Type)
|
||||
baseURL := strings.TrimRight(strings.TrimSpace(p.BaseURL), "/")
|
||||
if baseURL == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "base_url required for provider test"})
|
||||
return
|
||||
}
|
||||
|
||||
url := ""
|
||||
var req *http.Request
|
||||
|
||||
switch pt {
|
||||
case provider.TypeOpenAI, provider.TypeCompatible:
|
||||
url = baseURL + "/models"
|
||||
r, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to build request", "details": err.Error()})
|
||||
return
|
||||
}
|
||||
req = r
|
||||
if strings.TrimSpace(p.APIKey) != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+strings.TrimSpace(p.APIKey))
|
||||
}
|
||||
case provider.TypeAnthropic, provider.TypeClaude:
|
||||
url = baseURL + "/v1/models"
|
||||
r, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to build request", "details": err.Error()})
|
||||
return
|
||||
}
|
||||
req = r
|
||||
if strings.TrimSpace(p.APIKey) != "" {
|
||||
req.Header.Set("x-api-key", strings.TrimSpace(p.APIKey))
|
||||
}
|
||||
req.Header.Set("anthropic-version", "2023-06-01")
|
||||
default:
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "provider type not supported for test"})
|
||||
return
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, testProviderResponse{StatusCode: 0, OK: false, URL: url, Body: err.Error()})
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
|
||||
ok = resp.StatusCode >= 200 && resp.StatusCode < 300
|
||||
|
||||
c.JSON(http.StatusOK, testProviderResponse{
|
||||
StatusCode: resp.StatusCode,
|
||||
OK: ok,
|
||||
URL: url,
|
||||
Body: string(body),
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user