Files
ez-api/internal/api/batch_handler.go
zenfun 5349c9c833 feat(api): add admin master key listing/revoke
Add admin endpoints to list and revoke child keys under a master.
Standardize OpenAPI responses to use ResponseEnvelope with MapData
for error payloads, and regenerate swagger specs accordingly.
2026-01-10 01:10:36 +08:00

313 lines
9.4 KiB
Go

package api
import (
"errors"
"net/http"
"strings"
"github.com/ez-api/ez-api/internal/model"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type BatchActionRequest struct {
Action string `json:"action"`
IDs []uint `json:"ids"`
Status string `json:"status,omitempty"`
}
type BatchResult struct {
ID uint `json:"id"`
Error string `json:"error,omitempty"`
}
type BatchResponse struct {
Action string `json:"action"`
Success []uint `json:"success"`
Failed []BatchResult `json:"failed"`
}
func normalizeBatchAction(raw string) string {
raw = strings.ToLower(strings.TrimSpace(raw))
if raw == "" {
return "delete"
}
return raw
}
func normalizeStatus(raw string) string {
return strings.ToLower(strings.TrimSpace(raw))
}
func isAllowedStatus(raw string, allowed ...string) bool {
raw = normalizeStatus(raw)
for _, cand := range allowed {
if raw == cand {
return true
}
}
return false
}
// BatchMasters godoc
// @Summary Batch masters
// @Description Batch delete or status update for masters
// @Tags admin
// @Accept json
// @Produce json
// @Security AdminAuth
// @Param request body BatchActionRequest true "Batch payload"
// @Success 200 {object} ResponseEnvelope{data=BatchResponse}
// @Failure 400 {object} ResponseEnvelope{data=MapData}
// @Failure 500 {object} ResponseEnvelope{data=MapData}
// @Router /admin/masters/batch [post]
func (h *AdminHandler) BatchMasters(c *gin.Context) {
var req BatchActionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
action := normalizeBatchAction(req.Action)
if action != "delete" && action != "status" {
c.JSON(http.StatusBadRequest, gin.H{"error": "unsupported action"})
return
}
if len(req.IDs) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "ids required"})
return
}
resp := BatchResponse{Action: action}
status := normalizeStatus(req.Status)
if action == "status" && !isAllowedStatus(status, "active", "suspended") {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid status"})
return
}
for _, id := range req.IDs {
var err error
switch action {
case "delete":
err = h.revokeMasterByID(id)
case "status":
err = h.setMasterStatusByID(id, status)
}
if err != nil {
if isRecordNotFound(err) {
resp.Failed = append(resp.Failed, BatchResult{ID: id, Error: "not found"})
continue
}
resp.Failed = append(resp.Failed, BatchResult{ID: id, Error: err.Error()})
continue
}
resp.Success = append(resp.Success, id)
}
c.JSON(http.StatusOK, resp)
}
// BatchAPIKeys godoc
// @Summary Batch api keys
// @Description Batch delete or status update for api keys
// @Tags admin
// @Accept json
// @Produce json
// @Security AdminAuth
// @Param request body BatchActionRequest true "Batch payload"
// @Success 200 {object} ResponseEnvelope{data=BatchResponse}
// @Failure 400 {object} ResponseEnvelope{data=MapData}
// @Failure 500 {object} ResponseEnvelope{data=MapData}
// @Router /admin/api-keys/batch [post]
func (h *Handler) BatchAPIKeys(c *gin.Context) {
var req BatchActionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
action := normalizeBatchAction(req.Action)
if action != "delete" && action != "status" {
c.JSON(http.StatusBadRequest, gin.H{"error": "unsupported action"})
return
}
if len(req.IDs) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "ids required"})
return
}
resp := BatchResponse{Action: action}
status := normalizeStatus(req.Status)
if action == "status" && !isAllowedStatus(status, "active", "auto_disabled", "manual_disabled") {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid status"})
return
}
needsBindingSync := false
for _, id := range req.IDs {
var key model.APIKey
if err := h.db.First(&key, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
resp.Failed = append(resp.Failed, BatchResult{ID: id, Error: "not found"})
continue
}
resp.Failed = append(resp.Failed, BatchResult{ID: id, Error: err.Error()})
continue
}
switch action {
case "delete":
if err := h.db.Delete(&key).Error; err != nil {
resp.Failed = append(resp.Failed, BatchResult{ID: id, Error: err.Error()})
continue
}
resp.Success = append(resp.Success, id)
needsBindingSync = true
case "status":
update := map[string]any{"status": status}
if status == "active" {
update["ban_reason"] = ""
update["ban_until"] = nil
}
if err := h.db.Model(&key).Updates(update).Error; err != nil {
resp.Failed = append(resp.Failed, BatchResult{ID: id, Error: err.Error()})
continue
}
if err := h.db.First(&key, id).Error; err != nil {
resp.Failed = append(resp.Failed, BatchResult{ID: id, Error: err.Error()})
continue
}
resp.Success = append(resp.Success, id)
needsBindingSync = true
}
}
if needsBindingSync {
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, resp)
}
// BatchModels godoc
// @Summary Batch models
// @Description Batch delete for models
// @Tags admin
// @Accept json
// @Produce json
// @Security AdminAuth
// @Param request body BatchActionRequest true "Batch payload"
// @Success 200 {object} ResponseEnvelope{data=BatchResponse}
// @Failure 400 {object} ResponseEnvelope{data=MapData}
// @Failure 500 {object} ResponseEnvelope{data=MapData}
// @Router /admin/models/batch [post]
func (h *Handler) BatchModels(c *gin.Context) {
var req BatchActionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
action := normalizeBatchAction(req.Action)
if action != "delete" {
c.JSON(http.StatusBadRequest, gin.H{"error": "unsupported action"})
return
}
if len(req.IDs) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "ids required"})
return
}
resp := BatchResponse{Action: action}
for _, id := range req.IDs {
var m model.Model
if err := h.db.First(&m, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
resp.Failed = append(resp.Failed, BatchResult{ID: id, Error: "not found"})
continue
}
resp.Failed = append(resp.Failed, BatchResult{ID: id, Error: err.Error()})
continue
}
if err := h.db.Delete(&m).Error; err != nil {
resp.Failed = append(resp.Failed, BatchResult{ID: id, Error: err.Error()})
continue
}
if err := h.sync.SyncModelDelete(&m); err != nil {
resp.Failed = append(resp.Failed, BatchResult{ID: id, Error: err.Error()})
continue
}
resp.Success = append(resp.Success, id)
}
c.JSON(http.StatusOK, resp)
}
// BatchBindings godoc
// @Summary Batch bindings
// @Description Batch delete or status update for bindings
// @Tags admin
// @Accept json
// @Produce json
// @Security AdminAuth
// @Param request body BatchActionRequest true "Batch payload"
// @Success 200 {object} ResponseEnvelope{data=BatchResponse}
// @Failure 400 {object} ResponseEnvelope{data=MapData}
// @Failure 500 {object} ResponseEnvelope{data=MapData}
// @Router /admin/bindings/batch [post]
func (h *Handler) BatchBindings(c *gin.Context) {
var req BatchActionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
action := normalizeBatchAction(req.Action)
if action != "delete" && action != "status" {
c.JSON(http.StatusBadRequest, gin.H{"error": "unsupported action"})
return
}
if len(req.IDs) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "ids required"})
return
}
resp := BatchResponse{Action: action}
needsSync := false
status := normalizeStatus(req.Status)
if action == "status" && status == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "status required"})
return
}
for _, id := range req.IDs {
var b model.Binding
if err := h.db.First(&b, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
resp.Failed = append(resp.Failed, BatchResult{ID: id, Error: "not found"})
continue
}
resp.Failed = append(resp.Failed, BatchResult{ID: id, Error: err.Error()})
continue
}
switch action {
case "delete":
if err := h.db.Delete(&b).Error; err != nil {
resp.Failed = append(resp.Failed, BatchResult{ID: id, Error: err.Error()})
continue
}
resp.Success = append(resp.Success, id)
needsSync = true
case "status":
if err := h.db.Model(&b).Update("status", status).Error; err != nil {
resp.Failed = append(resp.Failed, BatchResult{ID: id, Error: err.Error()})
continue
}
resp.Success = append(resp.Success, id)
needsSync = true
}
}
if needsSync {
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, resp)
}