420 lines
15 KiB
Go
420 lines
15 KiB
Go
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...)
|
|
}
|
|
}
|
|
}
|