ResumeLens/internal/api/middleware.go
2026-04-07 13:14:52 -07:00

132 lines
3.9 KiB
Go

package api
import (
"net"
"net/http"
"slices"
"strings"
"sync"
"time"
)
// CORS allows requests from the frontend.
// Supports both development (Vite dev server) and production (nginx/Docker) environments.
func CORS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
// Allow requests from common development and production origins
allowedOrigins := []string{
"http://localhost:5173", // Vite dev server
"http://localhost", // Docker nginx
"http://localhost:80", // Docker nginx with port
}
// Check if the origin is allowed
allowed := slices.Contains(allowedOrigins, origin)
// Set CORS headers if origin is allowed
if allowed {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
w.Header().Set("Access-Control-Allow-Credentials", "true")
}
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
// requestHistory stores timestamps of requests for each IP address
type requestHistory struct {
mu sync.Mutex
timestamps map[string][]time.Time
}
var rateLimiter = &requestHistory{
timestamps: make(map[string][]time.Time),
}
var timeNow = time.Now
// RateLimit restricts requests to 10 per hour per IP address
// Trace: SRD_FuncReq_0014 - Prevent users from grading more than 10 resumes per hour
// Trace: SRD_SecReq_0004 - Implement rate limit of 10 requests per hour
// Trace: SRD_SecReq_0006 - Use device sending IP to impose rate limit
// Trace: SDD_LLD_0011 - Restrict request frequency to 10 calls per hour per unique source IP
// Trace: SDD_HLD_0006 - Function only within call rate limit of 10 calls per hour
func RateLimit(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Extract client IP address
// Trace: SRD_SecReq_0006 - Use device sending IP to impose rate limit
ip := getClientIP(r)
rateLimiter.mu.Lock()
defer rateLimiter.mu.Unlock()
now := timeNow()
oneHourAgo := now.Add(-1 * time.Hour)
// Get request history for this IP
history := rateLimiter.timestamps[ip]
// Filter out requests older than 1 hour
var recentRequests []time.Time
for _, timestamp := range history {
if timestamp.After(oneHourAgo) {
recentRequests = append(recentRequests, timestamp)
}
}
// Check if rate limit exceeded
// Trace: SDD_LLD_0011 - Restrict to 10 calls per hour per IP
if len(recentRequests) >= 10 {
// Trace: SRD_UseCase_0005, SRD_UseCase_0006 - Rate limit error handling
// Trace: SRD_QualAssurReq_0006 - Return error when rate limit reached
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusTooManyRequests)
w.Write([]byte(`{"error": "Rate limit exceeded. You can make up to 10 requests per hour. Please try again later."}`))
return
}
// Add current request to history
recentRequests = append(recentRequests, now)
rateLimiter.timestamps[ip] = recentRequests
// Allow request to proceed
next.ServeHTTP(w, r)
})
}
// getClientIP extracts the real client IP address from the request
// Trace: SRD_SecReq_0006 - Use device sending IP to impose rate limit
func getClientIP(r *http.Request) string {
// Check X-Forwarded-For header (for requests behind proxies/load balancers)
xff := r.Header.Get("X-Forwarded-For")
if xff != "" {
// X-Forwarded-For can contain multiple IPs, take the first one
ips := strings.Split(xff, ",")
if len(ips) > 0 {
return strings.TrimSpace(ips[0])
}
}
// Check X-Real-IP header (alternative proxy header)
xri := r.Header.Get("X-Real-IP")
if xri != "" {
return xri
}
// Fall back to RemoteAddr (direct connection)
ip, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
return r.RemoteAddr
}
return ip
}