diff --git a/cmd/server/main.go b/cmd/server/main.go index a5c02b2..bb79952 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -4,6 +4,7 @@ import ( "bookmann/internal/config" "bookmann/internal/database" "bookmann/internal/handlers" + ratelimit "bookmann/internal/middleware" "bookmann/templates" "bytes" "context" @@ -17,7 +18,7 @@ import ( "github.com/jackc/pgx/v5/pgxpool" "github.com/labstack/echo-jwt/v4" "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" + echomiddleware "github.com/labstack/echo/v4/middleware" ) // CustomValidator wraps the go-playground validator @@ -49,13 +50,17 @@ func main() { e.Validator = &CustomValidator{validator: validator.New()} // Middleware - e.Use(middleware.Logger()) - e.Use(middleware.Recover()) - e.Use(middleware.CORS()) + e.Use(echomiddleware.Logger()) + e.Use(echomiddleware.Recover()) + e.Use(echomiddleware.CORS()) - // Auth routes (no auth required) - e.POST("/api/auth/register", authHandler.Register) - e.POST("/api/auth/login", authHandler.Login) + // Rate limiter for auth endpoints + rateLimiter := ratelimit.NewRateLimiter(ratelimit.DefaultRateLimiterConfig()) + rateLimitMiddleware := ratelimit.RateLimiterMiddleware(rateLimiter) + + // Auth routes (no auth required, but rate limited) + e.POST("/api/auth/register", rateLimitMiddleware(authHandler.Register)) + e.POST("/api/auth/login", rateLimitMiddleware(authHandler.Login)) // JWT middleware for protected routes jwtMiddleware := echojwt.WithConfig(echojwt.Config{ diff --git a/internal/middleware/rate_limiter.go b/internal/middleware/rate_limiter.go new file mode 100644 index 0000000..efc251c --- /dev/null +++ b/internal/middleware/rate_limiter.go @@ -0,0 +1,116 @@ +package middleware + +import ( + "net/http" + "sync" + "time" + + "github.com/labstack/echo/v4" +) + +// RateLimiterConfig defines rate limiting configuration +type RateLimiterConfig struct { + RequestsPerMinute int + CleanupInterval time.Duration +} + +// DefaultRateLimiterConfig returns sensible defaults +func DefaultRateLimiterConfig() RateLimiterConfig { + return RateLimiterConfig{ + RequestsPerMinute: 10, + CleanupInterval: 5 * time.Minute, + } +} + +// RateLimiter tracks request counts per IP +type RateLimiter struct { + mu sync.RWMutex + requests map[string][]time.Time + config RateLimiterConfig +} + +// NewRateLimiter creates a new rate limiter +func NewRateLimiter(config RateLimiterConfig) *RateLimiter { + rl := &RateLimiter{ + requests: make(map[string][]time.Time), + config: config, + } + + // Start cleanup goroutine + go rl.cleanup() + + return rl +} + +// cleanup removes old request records +func (rl *RateLimiter) cleanup() { + ticker := time.NewTicker(rl.config.CleanupInterval) + defer ticker.Stop() + + for range ticker.C { + rl.mu.Lock() + now := time.Now() + for ip, requests := range rl.requests { + // Remove requests older than 1 minute + var valid []time.Time + for _, reqTime := range requests { + if now.Sub(reqTime) < time.Minute { + valid = append(valid, reqTime) + } + } + if len(valid) == 0 { + delete(rl.requests, ip) + } else { + rl.requests[ip] = valid + } + } + rl.mu.Unlock() + } +} + +// Allow checks if a request from the given IP should be allowed +func (rl *RateLimiter) Allow(ip string) bool { + rl.mu.Lock() + defer rl.mu.Unlock() + + now := time.Now() + + // Clean old requests for this IP + var valid []time.Time + for _, reqTime := range rl.requests[ip] { + if now.Sub(reqTime) < time.Minute { + valid = append(valid, reqTime) + } + } + rl.requests[ip] = valid + + // Check if limit exceeded + if len(rl.requests[ip]) >= rl.config.RequestsPerMinute { + return false + } + + // Add this request + rl.requests[ip] = append(rl.requests[ip], now) + return true +} + +// RateLimiterMiddleware returns echo middleware for rate limiting +func RateLimiterMiddleware(rl *RateLimiter) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + // Get client IP + ip := c.RealIP() + if ip == "" { + ip = c.Request().RemoteAddr + } + + if !rl.Allow(ip) { + return c.JSON(http.StatusTooManyRequests, map[string]string{ + "error": "too many requests, please try again later", + }) + } + + return next(c) + } + } +}