- Add NOT NULL constraints to soundtrack.uuid, song.uuid, and song.soundtrack_uuid - Add corresponding DROP NOT NULL in down migration - Update header comments to reflect combined Phase 1 & 2 This eliminates the need for a separate migration 000007 while maintaining proper rollback capability. The NOT NULL constraints are added AFTER all UUID backfills are complete, ensuring data integrity. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
99 lines
2.0 KiB
Go
99 lines
2.0 KiB
Go
package middleware
|
|
|
|
import (
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
|
|
"music-server/internal/logging"
|
|
|
|
"github.com/labstack/echo/v5"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// RateLimiter implements a simple IP-based rate limiter
|
|
type RateLimiter struct {
|
|
visits map[string]*Visit
|
|
mu sync.Mutex
|
|
limit int // requests per window
|
|
window time.Duration // time window
|
|
}
|
|
|
|
type Visit struct {
|
|
count int
|
|
firstSeen time.Time
|
|
}
|
|
|
|
// NewRateLimiter creates a new rate limiter
|
|
func NewRateLimiter(limit int, window time.Duration) *RateLimiter {
|
|
rl := &RateLimiter{
|
|
visits: make(map[string]*Visit),
|
|
limit: limit,
|
|
window: window,
|
|
}
|
|
|
|
// Start cleanup goroutine
|
|
go rl.cleanup()
|
|
return rl
|
|
}
|
|
|
|
// cleanup periodically removes old entries
|
|
func (rl *RateLimiter) cleanup() {
|
|
for {
|
|
time.Sleep(rl.window)
|
|
rl.mu.Lock()
|
|
now := time.Now()
|
|
for ip, visit := range rl.visits {
|
|
if now.Sub(visit.firstSeen) > rl.window {
|
|
delete(rl.visits, ip)
|
|
}
|
|
}
|
|
rl.mu.Unlock()
|
|
}
|
|
}
|
|
|
|
// RateLimitMiddleware returns an Echo middleware for rate limiting
|
|
func RateLimitMiddleware(rl *RateLimiter) echo.MiddlewareFunc {
|
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
|
return func(c *echo.Context) error {
|
|
ip := c.RealIP()
|
|
|
|
rl.mu.Lock()
|
|
defer rl.mu.Unlock()
|
|
|
|
visit, exists := rl.visits[ip]
|
|
now := time.Now()
|
|
|
|
if !exists {
|
|
// First visit from this IP
|
|
rl.visits[ip] = &Visit{
|
|
count: 1,
|
|
firstSeen: now,
|
|
}
|
|
return next(c)
|
|
}
|
|
|
|
// Check if window has expired
|
|
if now.Sub(visit.firstSeen) > rl.window {
|
|
// Reset for new window
|
|
visit.count = 1
|
|
visit.firstSeen = now
|
|
return next(c)
|
|
}
|
|
|
|
// Check if limit exceeded
|
|
if visit.count >= rl.limit {
|
|
logging.GetLogger().Warn("Rate limit exceeded",
|
|
zap.String("ip", ip),
|
|
zap.String("path", c.Request().URL.Path))
|
|
return c.JSON(http.StatusTooManyRequests, map[string]string{
|
|
"error": "Rate limit exceeded. Please try again later.",
|
|
})
|
|
}
|
|
|
|
// Increment count
|
|
visit.count++
|
|
return next(c)
|
|
}
|
|
}
|
|
} |