first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:45:19 +03:00
commit 60db80892b
101 changed files with 16757 additions and 0 deletions

122
middlewares/rate_limit.go Normal file
View File

@@ -0,0 +1,122 @@
package middlewares
import (
"context"
"encoding/json"
"errors"
"fmt"
configs "goFiber/config"
database "goFiber/database/config"
"goFiber/database/models"
"log"
"net/http"
"strconv"
"strings"
"time"
"github.com/gofiber/fiber/v3"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
type rateLimitRuntime struct {
Name string `json:"name"`
MaxRequests int64 `json:"max_requests"`
WindowSeconds int `json:"window_seconds"`
IsActive bool `json:"is_active"`
}
// RequireRateLimit applies Redis-backed per-IP rate limiting by setting name.
func RequireRateLimit(name string, fallbackMax int64, fallbackWindowSeconds int) fiber.Handler {
return func(c fiber.Ctx) error {
if database.DB == nil {
return c.Next()
}
setting, err := loadRateLimitRuntime(name, fallbackMax, fallbackWindowSeconds)
if err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "rate limit configuration error"})
}
if !setting.IsActive {
return c.Next()
}
if database.RedisClient == nil {
rateLimitLogf("[rate-limit][warn] redis unavailable, skipping enforcement name=%s", setting.Name)
return c.Next()
}
ip := strings.TrimSpace(c.IP())
if ip == "" {
ip = "unknown"
}
counterKey := fmt.Sprintf("ratelimit:%s:%s", setting.Name, ip)
count, err := database.RedisClient.Incr(context.Background(), counterKey).Result()
if err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "rate limit check failed"})
}
if count == 1 {
_ = database.RedisClient.Expire(context.Background(), counterKey, time.Duration(setting.WindowSeconds)*time.Second).Err()
}
if count > setting.MaxRequests {
ttl, _ := database.RedisClient.TTL(context.Background(), counterKey).Result()
retryAfter := int(ttl.Seconds())
if retryAfter < 1 {
retryAfter = setting.WindowSeconds
}
c.Set("Retry-After", strconv.Itoa(retryAfter))
log.Printf("[rate-limit][blocked] name=%s ip=%s count=%d max=%d window=%ds", setting.Name, ip, count, setting.MaxRequests, setting.WindowSeconds)
return c.Status(http.StatusTooManyRequests).JSON(fiber.Map{
"error": "too many requests",
"retry_after": retryAfter,
})
}
rateLimitLogf("[rate-limit][allow] name=%s ip=%s count=%d max=%d window=%ds", setting.Name, ip, count, setting.MaxRequests, setting.WindowSeconds)
return c.Next()
}
}
func loadRateLimitRuntime(name string, fallbackMax int64, fallbackWindowSeconds int) (*rateLimitRuntime, error) {
cacheKey := "ratelimit:setting:" + name
if cached, err := database.Get(cacheKey); err == nil {
var s rateLimitRuntime
if jsonErr := json.Unmarshal([]byte(cached), &s); jsonErr == nil {
return &s, nil
}
} else if !errors.Is(err, redis.Nil) {
return nil, err
}
setting := &rateLimitRuntime{
Name: name,
MaxRequests: fallbackMax,
WindowSeconds: fallbackWindowSeconds,
IsActive: true,
}
var dbSetting models.RateLimitSetting
if err := database.DB.Where("name = ?", name).First(&dbSetting).Error; err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
rateLimitLogf("[rate-limit][config] setting=%s not found, using fallback max=%d window=%ds", name, fallbackMax, fallbackWindowSeconds)
} else {
setting.MaxRequests = dbSetting.MaxRequests
setting.WindowSeconds = dbSetting.WindowSeconds
setting.IsActive = dbSetting.IsActive
rateLimitLogf("[rate-limit][config] loaded from db name=%s active=%t max=%d window=%ds", name, setting.IsActive, setting.MaxRequests, setting.WindowSeconds)
}
cacheJSON, _ := json.Marshal(setting)
_ = database.SetEx(cacheKey, string(cacheJSON), 60)
return setting, nil
}
func rateLimitLogf(format string, args ...interface{}) {
if configs.AppConfig != nil && configs.AppConfig.CorsDebug {
log.Printf(format, args...)
}
}