mirror of
https://github.com/EZ-Api/ez-api.git
synced 2026-01-13 17:47:51 +00:00
feat(server): integrate ip ban cron and refine updates
- Initialize and schedule IP ban maintenance tasks in server entry point - Perform initial IP ban sync to Redis on startup - Implement optional JSON unmarshalling to handle null `expires_at` in API - Add CIDR overlap validation when updating rule status to active
This commit is contained in:
@@ -235,6 +235,7 @@ func main() {
|
|||||||
authHandler := api.NewAuthHandler(db, rdb, adminService, masterService)
|
authHandler := api.NewAuthHandler(db, rdb, adminService, masterService)
|
||||||
ipBanService := service.NewIPBanService(db, rdb)
|
ipBanService := service.NewIPBanService(db, rdb)
|
||||||
ipBanHandler := api.NewIPBanHandler(ipBanService)
|
ipBanHandler := api.NewIPBanHandler(ipBanService)
|
||||||
|
ipBanManager := cron.NewIPBanManager(ipBanService)
|
||||||
modelRegistryService := service.NewModelRegistryService(db, rdb, service.ModelRegistryConfig{
|
modelRegistryService := service.NewModelRegistryService(db, rdb, service.ModelRegistryConfig{
|
||||||
Enabled: cfg.ModelRegistry.Enabled,
|
Enabled: cfg.ModelRegistry.Enabled,
|
||||||
RefreshEvery: time.Duration(cfg.ModelRegistry.RefreshSeconds) * time.Second,
|
RefreshEvery: time.Duration(cfg.ModelRegistry.RefreshSeconds) * time.Second,
|
||||||
@@ -250,11 +251,17 @@ func main() {
|
|||||||
if err := syncService.SyncAll(db); err != nil {
|
if err := syncService.SyncAll(db); err != nil {
|
||||||
logger.Warn("initial sync warning", "err", err)
|
logger.Warn("initial sync warning", "err", err)
|
||||||
}
|
}
|
||||||
|
if err := ipBanService.SyncAllToRedis(context.Background()); err != nil {
|
||||||
|
logger.Warn("initial IP ban sync warning", "err", err)
|
||||||
|
}
|
||||||
// Initial model registry refresh before scheduler starts
|
// Initial model registry refresh before scheduler starts
|
||||||
if modelRegistryService.Enabled() {
|
if modelRegistryService.Enabled() {
|
||||||
modelRegistryService.RunOnce(context.Background())
|
modelRegistryService.RunOnce(context.Background())
|
||||||
sched.Every("model-registry-refresh", modelRegistryService.RefreshEvery(), modelRegistryService.RunOnce)
|
sched.Every("model-registry-refresh", modelRegistryService.RefreshEvery(), modelRegistryService.RunOnce)
|
||||||
}
|
}
|
||||||
|
sched.Every("ip-ban-expire", time.Minute, ipBanManager.ExpireRunOnce)
|
||||||
|
sched.Every("ip-ban-hit-sync", 5*time.Minute, ipBanManager.HitSyncRunOnce)
|
||||||
|
sched.Every("ip-ban-full-sync", 5*time.Minute, ipBanManager.FullSyncRunOnce)
|
||||||
sched.Start()
|
sched.Start()
|
||||||
|
|
||||||
// 5. Setup Gin Router
|
// 5. Setup Gin Router
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -29,9 +30,9 @@ type CreateIPBanRequest struct {
|
|||||||
|
|
||||||
// UpdateIPBanRequest represents a request to update an IP ban.
|
// UpdateIPBanRequest represents a request to update an IP ban.
|
||||||
type UpdateIPBanRequest struct {
|
type UpdateIPBanRequest struct {
|
||||||
Reason *string `json:"reason,omitempty"`
|
Reason *string `json:"reason,omitempty"`
|
||||||
ExpiresAt *int64 `json:"expires_at,omitempty"`
|
ExpiresAt optionalInt64 `json:"expires_at,omitempty"`
|
||||||
Status *string `json:"status,omitempty"`
|
Status *string `json:"status,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// IPBanView represents the API response for an IP ban.
|
// IPBanView represents the API response for an IP ban.
|
||||||
@@ -47,6 +48,25 @@ type IPBanView struct {
|
|||||||
UpdatedAt int64 `json:"updated_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 {
|
func toIPBanView(ban *model.IPBan) IPBanView {
|
||||||
return IPBanView{
|
return IPBanView{
|
||||||
ID: ban.ID,
|
ID: ban.ID,
|
||||||
@@ -183,6 +203,7 @@ func (h *IPBanHandler) Get(c *gin.Context) {
|
|||||||
// @Success 200 {object} IPBanView
|
// @Success 200 {object} IPBanView
|
||||||
// @Failure 400 {object} gin.H
|
// @Failure 400 {object} gin.H
|
||||||
// @Failure 404 {object} gin.H
|
// @Failure 404 {object} gin.H
|
||||||
|
// @Failure 409 {object} gin.H
|
||||||
// @Failure 500 {object} gin.H
|
// @Failure 500 {object} gin.H
|
||||||
// @Router /admin/ip-bans/{id} [put]
|
// @Router /admin/ip-bans/{id} [put]
|
||||||
func (h *IPBanHandler) Update(c *gin.Context) {
|
func (h *IPBanHandler) Update(c *gin.Context) {
|
||||||
@@ -199,15 +220,19 @@ func (h *IPBanHandler) Update(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ban, err := h.ipBanService.Update(c.Request.Context(), uint(id), service.UpdateIPBanRequest{
|
ban, err := h.ipBanService.Update(c.Request.Context(), uint(id), service.UpdateIPBanRequest{
|
||||||
Reason: req.Reason,
|
Reason: req.Reason,
|
||||||
ExpiresAt: req.ExpiresAt,
|
ExpiresAt: req.ExpiresAt.Value,
|
||||||
Status: req.Status,
|
ExpiresAtSet: req.ExpiresAt.Set,
|
||||||
|
Status: req.Status,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, service.ErrIPBanNotFound) {
|
switch {
|
||||||
|
case errors.Is(err, service.ErrIPBanNotFound):
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": "IP ban not found"})
|
c.JSON(http.StatusNotFound, gin.H{"error": "IP ban not found"})
|
||||||
} else {
|
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()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update IP ban", "details": err.Error()})
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrInvalidCIDR = errors.New("invalid CIDR format")
|
ErrInvalidCIDR = errors.New("invalid CIDR format")
|
||||||
ErrCIDROverlap = errors.New("CIDR overlaps with existing active rule")
|
ErrCIDROverlap = errors.New("CIDR overlaps with existing active rule")
|
||||||
ErrIPBanNotFound = errors.New("IP ban not found")
|
ErrIPBanNotFound = errors.New("IP ban not found")
|
||||||
ErrDuplicateCIDR = errors.New("CIDR already exists")
|
ErrDuplicateCIDR = errors.New("CIDR already exists")
|
||||||
)
|
)
|
||||||
|
|
||||||
// IPBanService handles global IP ban operations.
|
// IPBanService handles global IP ban operations.
|
||||||
@@ -93,9 +93,10 @@ type CreateIPBanRequest struct {
|
|||||||
|
|
||||||
// UpdateIPBanRequest represents a request to update an IP ban.
|
// UpdateIPBanRequest represents a request to update an IP ban.
|
||||||
type UpdateIPBanRequest struct {
|
type UpdateIPBanRequest struct {
|
||||||
Reason *string `json:"reason,omitempty"`
|
Reason *string `json:"reason,omitempty"`
|
||||||
ExpiresAt *int64 `json:"expires_at,omitempty"` // Use pointer to distinguish between "not set" and "set to null"
|
ExpiresAt *int64 `json:"expires_at,omitempty"`
|
||||||
Status *string `json:"status,omitempty"`
|
ExpiresAtSet bool `json:"-"`
|
||||||
|
Status *string `json:"status,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create creates a new IP ban with validation.
|
// Create creates a new IP ban with validation.
|
||||||
@@ -180,11 +181,25 @@ func (s *IPBanService) Update(ctx context.Context, id uint, req UpdateIPBanReque
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if req.Status != nil && *req.Status == model.IPBanStatusActive && ban.Status != model.IPBanStatusActive {
|
||||||
|
var activeRules []model.IPBan
|
||||||
|
if err := s.db.WithContext(ctx).
|
||||||
|
Where("status = ? AND id <> ?", model.IPBanStatusActive, ban.ID).
|
||||||
|
Find(&activeRules).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, rule := range activeRules {
|
||||||
|
if CIDROverlaps(ban.CIDR, rule.CIDR) {
|
||||||
|
return nil, fmt.Errorf("%w: overlaps with %s", ErrCIDROverlap, rule.CIDR)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
updates := make(map[string]interface{})
|
updates := make(map[string]interface{})
|
||||||
if req.Reason != nil {
|
if req.Reason != nil {
|
||||||
updates["reason"] = *req.Reason
|
updates["reason"] = *req.Reason
|
||||||
}
|
}
|
||||||
if req.ExpiresAt != nil {
|
if req.ExpiresAtSet {
|
||||||
updates["expires_at"] = req.ExpiresAt
|
updates["expires_at"] = req.ExpiresAt
|
||||||
}
|
}
|
||||||
if req.Status != nil {
|
if req.Status != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user