Files
aresv2/controllers/security_controller.go
Beyhan Oğur 4362c3b83f first commit
2026-04-26 21:33:39 +03:00

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...)
}
}
}