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:
zenfun
2026-01-04 01:44:45 +08:00
parent 830c6fa6e7
commit a7571dd4ad
3 changed files with 63 additions and 16 deletions

View File

@@ -235,6 +235,7 @@ func main() {
authHandler := api.NewAuthHandler(db, rdb, adminService, masterService)
ipBanService := service.NewIPBanService(db, rdb)
ipBanHandler := api.NewIPBanHandler(ipBanService)
ipBanManager := cron.NewIPBanManager(ipBanService)
modelRegistryService := service.NewModelRegistryService(db, rdb, service.ModelRegistryConfig{
Enabled: cfg.ModelRegistry.Enabled,
RefreshEvery: time.Duration(cfg.ModelRegistry.RefreshSeconds) * time.Second,
@@ -250,11 +251,17 @@ func main() {
if err := syncService.SyncAll(db); err != nil {
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
if modelRegistryService.Enabled() {
modelRegistryService.RunOnce(context.Background())
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()
// 5. Setup Gin Router

View File

@@ -1,6 +1,7 @@
package api
import (
"encoding/json"
"errors"
"net/http"
"strconv"
@@ -30,7 +31,7 @@ type CreateIPBanRequest struct {
// UpdateIPBanRequest represents a request to update an IP ban.
type UpdateIPBanRequest struct {
Reason *string `json:"reason,omitempty"`
ExpiresAt *int64 `json:"expires_at,omitempty"`
ExpiresAt optionalInt64 `json:"expires_at,omitempty"`
Status *string `json:"status,omitempty"`
}
@@ -47,6 +48,25 @@ type IPBanView struct {
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,
@@ -183,6 +203,7 @@ func (h *IPBanHandler) Get(c *gin.Context) {
// @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) {
@@ -200,14 +221,18 @@ func (h *IPBanHandler) Update(c *gin.Context) {
ban, err := h.ipBanService.Update(c.Request.Context(), uint(id), service.UpdateIPBanRequest{
Reason: req.Reason,
ExpiresAt: req.ExpiresAt,
ExpiresAt: req.ExpiresAt.Value,
ExpiresAtSet: req.ExpiresAt.Set,
Status: req.Status,
})
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"})
} 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()})
}
return

View File

@@ -94,7 +94,8 @@ type CreateIPBanRequest struct {
// UpdateIPBanRequest represents a request to update an IP ban.
type UpdateIPBanRequest struct {
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"`
ExpiresAtSet bool `json:"-"`
Status *string `json:"status,omitempty"`
}
@@ -180,11 +181,25 @@ func (s *IPBanService) Update(ctx context.Context, id uint, req UpdateIPBanReque
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{})
if req.Reason != nil {
updates["reason"] = *req.Reason
}
if req.ExpiresAt != nil {
if req.ExpiresAtSet {
updates["expires_at"] = req.ExpiresAt
}
if req.Status != nil {