// internal/infrastructure/transport/http/middleware/rate_limit.go package middleware import ( "context" "net/http" "sync" "time" "service/internal/infrastructure/cache" "service/pkg/logger" // Pastikan import ini benar "github.com/gin-gonic/gin" "golang.org/x/time/rate" ) // RateLimitMiddleware creates rate limiting middleware using Redis cache func RateLimitMiddleware(cacheManager *cache.Manager) gin.HandlerFunc { return func(c *gin.Context) { clientIP := c.ClientIP() ctx, cancel := context.WithTimeout(c.Request.Context(), 2*time.Second) defer cancel() // Increment rate limit counter count, err := cacheManager.IncrementRateLimit(ctx, clientIP) if err != nil { // PERBAIKAN: Gunakan logger baru dengan konteks dan field terstruktur logger.Default().WithContext(ctx). Error("Failed to increment rate limit for IP", logger.ErrorField(err), logger.String("client_ip", clientIP), ) // Allow request to proceed if cache is unavailable c.Next() return } // Set TTL on first request if count == 1 { if err := cacheManager.SetRateLimit(ctx, clientIP, count); err != nil { // PERBAIKAN: Gunakan logger baru logger.Default().WithContext(ctx). Error("Failed to set rate limit TTL for IP", logger.ErrorField(err), logger.String("client_ip", clientIP), ) } } // Check if rate limit exceeded (60 requests per minute) if count > 60 { // PERBAIKAN: Tambahkan log saat rate limit terlampaui logger.Default().WithContext(ctx). Warn("Rate limit exceeded for IP", logger.String("client_ip", clientIP), logger.Int64("count", count), ) c.JSON(http.StatusTooManyRequests, gin.H{ "error": "Too many requests", "code": "RATE_LIMIT_EXCEEDED", "retry_after": 60, }) c.Abort() return } c.Next() } } // RateLimitByToken creates rate limiting middleware based on auth token func RateLimitByToken(cacheManager *cache.Manager, requestsPerMinute int) gin.HandlerFunc { return func(c *gin.Context) { token := c.GetHeader("Authorization") if token == "" { c.Next() return } // Extract token from Bearer format if len(token) > 7 && token[:7] == "Bearer " { token = token[7:] } ctx, cancel := context.WithTimeout(c.Request.Context(), 2*time.Second) defer cancel() // Increment rate limit counter count, err := cacheManager.IncrementRateLimit(ctx, "token:"+token) if err != nil { // PERBAIKAN: Gunakan logger baru logger.Default().WithContext(ctx). Error("Failed to increment token rate limit", logger.ErrorField(err), logger.String("token_prefix", token[:minLen(len(token), 10)]+"..."), // Jangan log token utuh ) c.Next() return } // Set TTL on first request if count == 1 { if err := cacheManager.SetRateLimit(ctx, "token:"+token, count); err != nil { // PERBAIKAN: Gunakan logger baru logger.Default().WithContext(ctx). Error("Failed to set token rate limit TTL", logger.ErrorField(err), logger.String("token_prefix", token[:minLen(len(token), 10)]+"..."), ) } } // Check if rate limit exceeded if count > int64(requestsPerMinute) { // PERBAIKAN: Tambahkan log saat rate limit token terlampaui logger.Default().WithContext(ctx). Warn("Rate limit exceeded for token", logger.String("token_prefix", token[:minLen(len(token), 10)]+"..."), logger.Int64("count", count), ) c.JSON(http.StatusTooManyRequests, gin.H{ "error": "Too many requests for this token", "code": "TOKEN_RATE_LIMIT_EXCEEDED", "retry_after": 60, }) c.Abort() return } c.Next() } } // Helper function untuk menghindari error jika token lebih pendek dari 10 karakter func minLen(a, b int) int { if a < b { return a } return b } // Variabel global untuk menyimpan limiter per-client IP/Identifier untuk memory rate limiter var ( visitors = make(map[string]*rate.Limiter) mu sync.Mutex ) // getVisitor mengambil atau membuat limiter baru untuk client identifier tertentu func getVisitor(identifier string, r rate.Limit, b int) *rate.Limiter { mu.Lock() defer mu.Unlock() limiter, exists := visitors[identifier] if !exists { limiter = rate.NewLimiter(r, b) visitors[identifier] = limiter } return limiter } // MemoryRateLimitMiddleware membatasi jumlah request per client secara lokal di memori. // requestsPerSecond: jumlah hit yang diizinkan per detik. // burstSize: jumlah hit maksimal dalam satu waktu (burst). func MemoryRateLimitMiddleware(requestsPerSecond float64, burstSize int) gin.HandlerFunc { limit := rate.Limit(requestsPerSecond) return func(c *gin.Context) { clientIdentifier := c.ClientIP() limiter := getVisitor(clientIdentifier, limit, burstSize) if !limiter.Allow() { c.JSON(http.StatusTooManyRequests, gin.H{ "status": "error", "message": "Terlalu banyak permintaan ke API Satu Sehat. Silakan coba beberapa saat lagi.", "code": "RATE_LIMIT_EXCEEDED", }) c.Abort() return } c.Next() } }