mirror of
https://github.com/EZ-Api/ez-api.git
synced 2026-01-13 17:47:51 +00:00
Decouple API contract from internal models by introducing dedicated DTOs for requests and responses. - Add Response DTOs for all resources (API Keys, Bindings, Models, Namespaces, etc.) - Update Swagger annotations to use DTOs with field examples instead of internal models - Refactor handlers to bind and return DTO structures - Consolidate request/response definitions in the dto package
309 lines
9.9 KiB
Go
309 lines
9.9 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=dto.APIKeyResponse}
|
|
// @Failure 400 {object} ResponseEnvelope{data=MapData}
|
|
// @Failure 500 {object} ResponseEnvelope{data=MapData}
|
|
// @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=[]dto.APIKeyResponse}
|
|
// @Failure 500 {object} ResponseEnvelope{data=MapData}
|
|
// @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=dto.APIKeyResponse}
|
|
// @Failure 400 {object} ResponseEnvelope{data=MapData}
|
|
// @Failure 404 {object} ResponseEnvelope{data=MapData}
|
|
// @Failure 500 {object} ResponseEnvelope{data=MapData}
|
|
// @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=dto.APIKeyResponse}
|
|
// @Failure 400 {object} ResponseEnvelope{data=MapData}
|
|
// @Failure 404 {object} ResponseEnvelope{data=MapData}
|
|
// @Failure 500 {object} ResponseEnvelope{data=MapData}
|
|
// @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=MapData}
|
|
// @Failure 400 {object} ResponseEnvelope{data=MapData}
|
|
// @Failure 404 {object} ResponseEnvelope{data=MapData}
|
|
// @Failure 500 {object} ResponseEnvelope{data=MapData}
|
|
// @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"})
|
|
}
|