package api import ( "encoding/json" "errors" "net/http" "strconv" "github.com/ez-api/ez-api/internal/model" "github.com/ez-api/ez-api/internal/service" "github.com/gin-gonic/gin" ) // IPBanHandler handles IP ban CRUD operations. type IPBanHandler struct { ipBanService *service.IPBanService } // NewIPBanHandler creates a new IPBanHandler. func NewIPBanHandler(ipBanService *service.IPBanService) *IPBanHandler { return &IPBanHandler{ipBanService: ipBanService} } // CreateIPBanRequest represents a request to create an IP ban. type CreateIPBanRequest struct { CIDR string `json:"cidr" binding:"required"` Reason string `json:"reason,omitempty"` ExpiresAt *int64 `json:"expires_at,omitempty"` } // UpdateIPBanRequest represents a request to update an IP ban. type UpdateIPBanRequest struct { Reason *string `json:"reason,omitempty"` ExpiresAt optionalInt64 `json:"expires_at,omitempty"` Status *string `json:"status,omitempty"` } // IPBanView represents the API response for an IP ban. type IPBanView struct { ID uint `json:"id"` CIDR string `json:"cidr"` Status string `json:"status"` Reason string `json:"reason,omitempty"` ExpiresAt *int64 `json:"expires_at,omitempty"` HitCount int64 `json:"hit_count"` CreatedBy string `json:"created_by,omitempty"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` } type optionalInt64 struct { Value *int64 Set bool } func (o *optionalInt64) UnmarshalJSON(data []byte) error { o.Set = true if string(data) == "null" { o.Value = nil return nil } var v int64 if err := json.Unmarshal(data, &v); err != nil { return err } o.Value = &v return nil } func toIPBanView(ban *model.IPBan) IPBanView { return IPBanView{ ID: ban.ID, CIDR: ban.CIDR, Status: ban.Status, Reason: ban.Reason, ExpiresAt: ban.ExpiresAt, HitCount: ban.HitCount, CreatedBy: ban.CreatedBy, CreatedAt: ban.CreatedAt.Unix(), UpdatedAt: ban.UpdatedAt.Unix(), } } // Create godoc // @Summary Create an IP ban // @Description Create a new global IP/CIDR ban rule // @Tags admin,ip-bans // @Accept json // @Produce json // @Security AdminAuth // @Param ban body CreateIPBanRequest true "IP Ban Info" // @Success 201 {object} IPBanView // @Failure 400 {object} gin.H // @Failure 409 {object} gin.H // @Failure 500 {object} gin.H // @Router /admin/ip-bans [post] func (h *IPBanHandler) Create(c *gin.Context) { var req CreateIPBanRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Get creator from context (set by admin auth middleware) createdBy := c.GetString("admin_user") if createdBy == "" { createdBy = "admin" } ban, err := h.ipBanService.Create(c.Request.Context(), service.CreateIPBanRequest{ CIDR: req.CIDR, Reason: req.Reason, ExpiresAt: req.ExpiresAt, CreatedBy: createdBy, }) if err != nil { switch { case errors.Is(err, service.ErrInvalidCIDR): c.JSON(http.StatusBadRequest, gin.H{"error": "invalid CIDR format"}) case errors.Is(err, service.ErrDuplicateCIDR): c.JSON(http.StatusConflict, gin.H{"error": "CIDR already exists"}) case errors.Is(err, service.ErrCIDROverlap): c.JSON(http.StatusConflict, gin.H{"error": err.Error()}) default: c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create IP ban", "details": err.Error()}) } return } c.JSON(http.StatusCreated, toIPBanView(ban)) } // List godoc // @Summary List IP bans // @Description List all global IP/CIDR ban rules // @Tags admin,ip-bans // @Accept json // @Produce json // @Security AdminAuth // @Param status query string false "Filter by status (active, expired)" // @Success 200 {array} IPBanView // @Failure 500 {object} gin.H // @Router /admin/ip-bans [get] func (h *IPBanHandler) List(c *gin.Context) { status := c.Query("status") bans, err := h.ipBanService.List(c.Request.Context(), status) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list IP bans", "details": err.Error()}) return } views := make([]IPBanView, len(bans)) for i, ban := range bans { views[i] = toIPBanView(&ban) } c.JSON(http.StatusOK, views) } // Get godoc // @Summary Get an IP ban // @Description Get a single global IP/CIDR ban rule by ID // @Tags admin,ip-bans // @Accept json // @Produce json // @Security AdminAuth // @Param id path int true "IP Ban ID" // @Success 200 {object} IPBanView // @Failure 404 {object} gin.H // @Failure 500 {object} gin.H // @Router /admin/ip-bans/{id} [get] func (h *IPBanHandler) Get(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"}) return } ban, err := h.ipBanService.Get(c.Request.Context(), uint(id)) if err != nil { if errors.Is(err, service.ErrIPBanNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": "IP ban not found"}) } else { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get IP ban", "details": err.Error()}) } return } c.JSON(http.StatusOK, toIPBanView(ban)) } // Update godoc // @Summary Update an IP ban // @Description Update a global IP/CIDR ban rule // @Tags admin,ip-bans // @Accept json // @Produce json // @Security AdminAuth // @Param id path int true "IP Ban ID" // @Param ban body UpdateIPBanRequest true "IP Ban Update" // @Success 200 {object} IPBanView // @Failure 400 {object} gin.H // @Failure 404 {object} gin.H // @Failure 409 {object} gin.H // @Failure 500 {object} gin.H // @Router /admin/ip-bans/{id} [put] func (h *IPBanHandler) Update(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"}) return } var req UpdateIPBanRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } ban, err := h.ipBanService.Update(c.Request.Context(), uint(id), service.UpdateIPBanRequest{ Reason: req.Reason, ExpiresAt: req.ExpiresAt.Value, ExpiresAtSet: req.ExpiresAt.Set, Status: req.Status, }) if err != nil { switch { case errors.Is(err, service.ErrIPBanNotFound): c.JSON(http.StatusNotFound, gin.H{"error": "IP ban not found"}) case errors.Is(err, service.ErrCIDROverlap): c.JSON(http.StatusConflict, gin.H{"error": err.Error()}) default: c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update IP ban", "details": err.Error()}) } return } c.JSON(http.StatusOK, toIPBanView(ban)) } // Delete godoc // @Summary Delete an IP ban // @Description Delete a global IP/CIDR ban rule // @Tags admin,ip-bans // @Accept json // @Produce json // @Security AdminAuth // @Param id path int true "IP Ban ID" // @Success 204 // @Failure 404 {object} gin.H // @Failure 500 {object} gin.H // @Router /admin/ip-bans/{id} [delete] func (h *IPBanHandler) Delete(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"}) return } if err := h.ipBanService.Delete(c.Request.Context(), uint(id)); err != nil { if errors.Is(err, service.ErrIPBanNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": "IP ban not found"}) } else { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete IP ban", "details": err.Error()}) } return } c.Status(http.StatusNoContent) }