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} BatchResponse // @Failure 400 {object} gin.H // @Failure 500 {object} gin.H // @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} BatchResponse // @Failure 400 {object} gin.H // @Failure 500 {object} gin.H // @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} BatchResponse // @Failure 400 {object} gin.H // @Failure 500 {object} gin.H // @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} BatchResponse // @Failure 400 {object} gin.H // @Failure 500 {object} gin.H // @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) }