package controllers import ( configs "ares/config" database "ares/database/config" "ares/database/models" "ares/middlewares" "encoding/json" "errors" "net/http" "strconv" "strings" "github.com/gofiber/fiber/v3" "github.com/redis/go-redis/v9" "gorm.io/gorm" ) const ( corsWhitelistCacheKey = "admin:cors:whitelist:list" corsBlacklistCacheKey = "admin:cors:blacklist:list" rateLimitCacheKey = "admin:rate_limit:list" securityCacheTTL = 60 ) type CorsWhitelistRequest struct { Origin string `json:"origin" validate:"required"` Description string `json:"description"` IsActive *bool `json:"is_active"` } type CorsBlacklistRequest struct { Origin string `json:"origin" validate:"required"` Reason string `json:"reason"` IsActive *bool `json:"is_active"` } type RateLimitSettingRequest struct { Name string `json:"name" validate:"required"` Description string `json:"description"` MaxRequests int64 `json:"max_requests" validate:"required,min=1"` WindowSeconds int `json:"window_seconds" validate:"required,min=1"` IsActive *bool `json:"is_active"` } func ListCorsWhitelists(c fiber.Ctx) error { if database.DB == nil { return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"}) } var items []models.CorsWhitelist if cached, err := database.Get(corsWhitelistCacheKey); err == nil { if unmarshalErr := json.Unmarshal([]byte(cached), &items); unmarshalErr == nil { securityLogf("[security][cors-whitelist][cache-hit] count=%d", len(items)) return c.JSON(fiber.Map{"count": len(items), "items": items}) } } else if !errors.Is(err, redis.Nil) { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "cache read error"}) } if err := database.DB.Order("id DESC").Find(&items).Error; err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"}) } cacheJSON, _ := json.Marshal(items) _ = database.SetEx(corsWhitelistCacheKey, string(cacheJSON), securityCacheTTL) securityLogf("[security][cors-whitelist][db-load] count=%d", len(items)) return c.JSON(fiber.Map{"count": len(items), "items": items}) } func CreateCorsWhitelist(c fiber.Ctx) error { var req CorsWhitelistRequest if err := c.Bind().JSON(&req); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"}) } if err := validate.Struct(req); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) } item := models.CorsWhitelist{ Origin: strings.TrimSpace(req.Origin), Description: strings.TrimSpace(req.Description), IsActive: boolValue(req.IsActive, true), CreatedBy: currentActor(c), } if err := database.DB.Create(&item).Error; err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "record could not be created"}) } invalidateSecurityCaches() securityLogf("[security][cors-whitelist][create] origin=%s by=%s", item.Origin, item.CreatedBy) return c.Status(http.StatusCreated).JSON(fiber.Map{"item": item}) } func UpdateCorsWhitelist(c fiber.Ctx) error { id, err := parseID(c.Params("id")) if err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"}) } var req CorsWhitelistRequest if err := c.Bind().JSON(&req); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"}) } if err := validate.Struct(req); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) } var item models.CorsWhitelist if err := database.DB.First(&item, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "record not found"}) } return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"}) } item.Origin = strings.TrimSpace(req.Origin) item.Description = strings.TrimSpace(req.Description) item.IsActive = boolValue(req.IsActive, item.IsActive) item.CreatedBy = currentActor(c) if err := database.DB.Save(&item).Error; err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "record could not be updated"}) } invalidateSecurityCaches() securityLogf("[security][cors-whitelist][update] id=%d origin=%s by=%s", item.ID, item.Origin, item.CreatedBy) return c.JSON(fiber.Map{"item": item}) } func DeleteCorsWhitelist(c fiber.Ctx) error { id, err := parseID(c.Params("id")) if err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"}) } if err := database.DB.Delete(&models.CorsWhitelist{}, id).Error; err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "record could not be deleted"}) } invalidateSecurityCaches() securityLogf("[security][cors-whitelist][soft-delete] id=%d by=%s", id, currentActor(c)) return c.JSON(fiber.Map{"message": "soft deleted", "id": id}) } func HardDeleteCorsWhitelist(c fiber.Ctx) error { id, err := parseID(c.Params("id")) if err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"}) } if err := database.DB.Unscoped().Delete(&models.CorsWhitelist{}, id).Error; err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "record could not be hard-deleted"}) } invalidateSecurityCaches() securityLogf("[security][cors-whitelist][hard-delete] id=%d by=%s", id, currentActor(c)) return c.JSON(fiber.Map{"message": "hard deleted", "id": id}) } func ListCorsBlacklists(c fiber.Ctx) error { if database.DB == nil { return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"}) } var items []models.CorsBlacklist if cached, err := database.Get(corsBlacklistCacheKey); err == nil { if unmarshalErr := json.Unmarshal([]byte(cached), &items); unmarshalErr == nil { securityLogf("[security][cors-blacklist][cache-hit] count=%d", len(items)) return c.JSON(fiber.Map{"count": len(items), "items": items}) } } else if !errors.Is(err, redis.Nil) { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "cache read error"}) } if err := database.DB.Order("id DESC").Find(&items).Error; err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"}) } cacheJSON, _ := json.Marshal(items) _ = database.SetEx(corsBlacklistCacheKey, string(cacheJSON), securityCacheTTL) securityLogf("[security][cors-blacklist][db-load] count=%d", len(items)) return c.JSON(fiber.Map{"count": len(items), "items": items}) } func CreateCorsBlacklist(c fiber.Ctx) error { var req CorsBlacklistRequest if err := c.Bind().JSON(&req); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"}) } if err := validate.Struct(req); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) } item := models.CorsBlacklist{ Origin: strings.TrimSpace(req.Origin), Reason: strings.TrimSpace(req.Reason), IsActive: boolValue(req.IsActive, true), CreatedBy: currentActor(c), } if err := database.DB.Create(&item).Error; err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "record could not be created"}) } invalidateSecurityCaches() securityLogf("[security][cors-blacklist][create] origin=%s by=%s", item.Origin, item.CreatedBy) return c.Status(http.StatusCreated).JSON(fiber.Map{"item": item}) } func UpdateCorsBlacklist(c fiber.Ctx) error { id, err := parseID(c.Params("id")) if err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"}) } var req CorsBlacklistRequest if err := c.Bind().JSON(&req); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"}) } if err := validate.Struct(req); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) } var item models.CorsBlacklist if err := database.DB.First(&item, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "record not found"}) } return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"}) } item.Origin = strings.TrimSpace(req.Origin) item.Reason = strings.TrimSpace(req.Reason) item.IsActive = boolValue(req.IsActive, item.IsActive) item.CreatedBy = currentActor(c) if err := database.DB.Save(&item).Error; err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "record could not be updated"}) } invalidateSecurityCaches() securityLogf("[security][cors-blacklist][update] id=%d origin=%s by=%s", item.ID, item.Origin, item.CreatedBy) return c.JSON(fiber.Map{"item": item}) } func DeleteCorsBlacklist(c fiber.Ctx) error { id, err := parseID(c.Params("id")) if err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"}) } if err := database.DB.Delete(&models.CorsBlacklist{}, id).Error; err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "record could not be deleted"}) } invalidateSecurityCaches() securityLogf("[security][cors-blacklist][soft-delete] id=%d by=%s", id, currentActor(c)) return c.JSON(fiber.Map{"message": "soft deleted", "id": id}) } func HardDeleteCorsBlacklist(c fiber.Ctx) error { id, err := parseID(c.Params("id")) if err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"}) } if err := database.DB.Unscoped().Delete(&models.CorsBlacklist{}, id).Error; err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "record could not be hard-deleted"}) } invalidateSecurityCaches() securityLogf("[security][cors-blacklist][hard-delete] id=%d by=%s", id, currentActor(c)) return c.JSON(fiber.Map{"message": "hard deleted", "id": id}) } func ListRateLimitSettings(c fiber.Ctx) error { if database.DB == nil { return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"}) } var items []models.RateLimitSetting if cached, err := database.Get(rateLimitCacheKey); err == nil { if unmarshalErr := json.Unmarshal([]byte(cached), &items); unmarshalErr == nil { securityLogf("[security][rate-limit][cache-hit] count=%d", len(items)) return c.JSON(fiber.Map{"count": len(items), "items": items}) } } else if !errors.Is(err, redis.Nil) { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "cache read error"}) } if err := database.DB.Order("id DESC").Find(&items).Error; err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"}) } cacheJSON, _ := json.Marshal(items) _ = database.SetEx(rateLimitCacheKey, string(cacheJSON), securityCacheTTL) securityLogf("[security][rate-limit][db-load] count=%d", len(items)) return c.JSON(fiber.Map{"count": len(items), "items": items}) } func CreateRateLimitSetting(c fiber.Ctx) error { var req RateLimitSettingRequest if err := c.Bind().JSON(&req); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"}) } if err := validate.Struct(req); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) } item := models.RateLimitSetting{ Name: strings.TrimSpace(req.Name), Description: strings.TrimSpace(req.Description), MaxRequests: req.MaxRequests, WindowSeconds: req.WindowSeconds, IsActive: boolValue(req.IsActive, true), UpdatedBy: currentActor(c), } if err := database.DB.Create(&item).Error; err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "record could not be created"}) } invalidateSecurityCaches() securityLogf("[security][rate-limit][create] name=%s max=%d window=%ds by=%s", item.Name, item.MaxRequests, item.WindowSeconds, item.UpdatedBy) return c.Status(http.StatusCreated).JSON(fiber.Map{"item": item}) } func UpdateRateLimitSetting(c fiber.Ctx) error { id, err := parseID(c.Params("id")) if err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"}) } var req RateLimitSettingRequest if err := c.Bind().JSON(&req); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"}) } if err := validate.Struct(req); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) } var item models.RateLimitSetting if err := database.DB.First(&item, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "record not found"}) } return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"}) } item.Name = strings.TrimSpace(req.Name) item.Description = strings.TrimSpace(req.Description) item.MaxRequests = req.MaxRequests item.WindowSeconds = req.WindowSeconds item.IsActive = boolValue(req.IsActive, item.IsActive) item.UpdatedBy = currentActor(c) if err := database.DB.Save(&item).Error; err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "record could not be updated"}) } invalidateSecurityCaches() securityLogf("[security][rate-limit][update] id=%d name=%s max=%d window=%ds by=%s", item.ID, item.Name, item.MaxRequests, item.WindowSeconds, item.UpdatedBy) return c.JSON(fiber.Map{"item": item}) } func DeleteRateLimitSetting(c fiber.Ctx) error { id, err := parseID(c.Params("id")) if err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"}) } if err := database.DB.Delete(&models.RateLimitSetting{}, id).Error; err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "record could not be deleted"}) } invalidateSecurityCaches() securityLogf("[security][rate-limit][soft-delete] id=%d by=%s", id, currentActor(c)) return c.JSON(fiber.Map{"message": "soft deleted", "id": id}) } func HardDeleteRateLimitSetting(c fiber.Ctx) error { id, err := parseID(c.Params("id")) if err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"}) } if err := database.DB.Unscoped().Delete(&models.RateLimitSetting{}, id).Error; err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "record could not be hard-deleted"}) } invalidateSecurityCaches() securityLogf("[security][rate-limit][hard-delete] id=%d by=%s", id, currentActor(c)) return c.JSON(fiber.Map{"message": "hard deleted", "id": id}) } func parseID(param string) (uint, error) { v, err := strconv.ParseUint(strings.TrimSpace(param), 10, 64) if err != nil || v == 0 { return 0, errors.New("invalid id") } return uint(v), nil } func invalidateSecurityCaches() { _ = database.Delete(corsWhitelistCacheKey) _ = database.Delete(corsBlacklistCacheKey) _ = database.Delete(rateLimitCacheKey) _ = database.Delete("cors:active:whitelist") _ = database.Delete("cors:active:blacklist") } func currentActor(c fiber.Ctx) string { if claims, ok := middlewares.GetAuthClaims(c); ok && strings.TrimSpace(claims.Email) != "" { return claims.Email } return "system" } func boolValue(v *bool, fallback bool) bool { if v == nil { return fallback } return *v } func securityLogf(format string, args ...interface{}) { if configs.AppConfig != nil && configs.AppConfig.CorsDebug { if configs.Logger != nil { configs.Logger.Sugar().Infof(format, args...) } } }