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), } // 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 := time.Now() 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 }