package middleware import ( "fmt" "net/http" "strings" "sync" "time" c "github.com/juanatsap/cv-site/internal/constants" ) // contactRateLimitEntry tracks rate limiting for contact form per IP type contactRateLimitEntry struct { count int resetTime time.Time } // ContactRateLimiter provides rate limiting specifically for contact form // Allows 5 submissions per hour per IP address type ContactRateLimiter struct { mu sync.RWMutex clients map[string]*contactRateLimitEntry } // NewContactRateLimiter creates a new contact form rate limiter // Default: 5 requests per hour per IP func NewContactRateLimiter() *ContactRateLimiter { rl := &ContactRateLimiter{ clients: make(map[string]*contactRateLimitEntry), } // Cleanup expired entries every 10 minutes go rl.cleanup() return rl } // Middleware returns rate limiting middleware for contact form func (rl *ContactRateLimiter) Middleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Get client IP (handle X-Forwarded-For for proxies) ip := r.Header.Get(c.HeaderXForwardedFor) if ip == "" { ip = r.Header.Get(c.HeaderXRealIP) } if ip == "" { ip = strings.Split(r.RemoteAddr, ":")[0] } // Extract first IP if multiple IPs in X-Forwarded-For if strings.Contains(ip, ",") { ip = strings.TrimSpace(strings.Split(ip, ",")[0]) } if !rl.allow(ip) { // Check if HTMX request isHTMX := r.Header.Get(c.HeaderHXRequest) != "" if isHTMX { // Return HTMX-friendly error w.Header().Set(c.HeaderContentType, c.ContentTypeHTML) w.WriteHeader(http.StatusTooManyRequests) _, _ = w.Write([]byte(`
You've submitted too many contact forms. Please wait an hour before trying again.