 131868bdca
			
		
	
	131868bdca
	
	
	
		
			
			Major security, observability, and configuration improvements:
## Security Hardening
- Implemented configurable CORS (no more wildcards)
- Added comprehensive auth middleware for admin endpoints
- Enhanced webhook HMAC validation
- Added input validation and rate limiting
- Security headers and CSP policies
## Configuration Management
- Made N8N webhook URL configurable (WHOOSH_N8N_BASE_URL)
- Replaced all hardcoded endpoints with environment variables
- Added feature flags for LLM vs heuristic composition
- Gitea fetch hardening with EAGER_FILTER and FULL_RESCAN options
## API Completeness
- Implemented GetCouncilComposition function
- Added GET /api/v1/councils/{id} endpoint
- Council artifacts API (POST/GET /api/v1/councils/{id}/artifacts)
- /admin/health/details endpoint with component status
- Database lookup for repository URLs (no hardcoded fallbacks)
## Observability & Performance
- Added OpenTelemetry distributed tracing with goal/pulse correlation
- Performance optimization database indexes
- Comprehensive health monitoring
- Enhanced logging and error handling
## Infrastructure
- Production-ready P2P discovery (replaces mock implementation)
- Removed unused Redis configuration
- Enhanced Docker Swarm integration
- Added migration files for performance indexes
## Code Quality
- Comprehensive input validation
- Graceful error handling and failsafe fallbacks
- Backwards compatibility maintained
- Following security best practices
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
		
	
		
			
				
	
	
		
			145 lines
		
	
	
		
			2.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			145 lines
		
	
	
		
			2.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package auth
 | |
| 
 | |
| import (
 | |
| 	"fmt"
 | |
| 	"net/http"
 | |
| 	"sync"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/rs/zerolog/log"
 | |
| )
 | |
| 
 | |
| // RateLimiter implements a simple in-memory rate limiter
 | |
| type RateLimiter struct {
 | |
| 	mu       sync.RWMutex
 | |
| 	buckets  map[string]*bucket
 | |
| 	requests int
 | |
| 	window   time.Duration
 | |
| 	cleanup  time.Duration
 | |
| }
 | |
| 
 | |
| type bucket struct {
 | |
| 	count     int
 | |
| 	lastReset time.Time
 | |
| }
 | |
| 
 | |
| // NewRateLimiter creates a new rate limiter
 | |
| func NewRateLimiter(requests int, window time.Duration) *RateLimiter {
 | |
| 	rl := &RateLimiter{
 | |
| 		buckets:  make(map[string]*bucket),
 | |
| 		requests: requests,
 | |
| 		window:   window,
 | |
| 		cleanup:  window * 2,
 | |
| 	}
 | |
| 	
 | |
| 	// Start cleanup goroutine
 | |
| 	go rl.cleanupRoutine()
 | |
| 	
 | |
| 	return rl
 | |
| }
 | |
| 
 | |
| // Allow checks if a request should be allowed
 | |
| func (rl *RateLimiter) Allow(key string) bool {
 | |
| 	rl.mu.Lock()
 | |
| 	defer rl.mu.Unlock()
 | |
| 	
 | |
| 	now := time.Now()
 | |
| 	
 | |
| 	// Get or create bucket
 | |
| 	b, exists := rl.buckets[key]
 | |
| 	if !exists {
 | |
| 		rl.buckets[key] = &bucket{
 | |
| 			count:     1,
 | |
| 			lastReset: now,
 | |
| 		}
 | |
| 		return true
 | |
| 	}
 | |
| 	
 | |
| 	// Check if window has expired
 | |
| 	if now.Sub(b.lastReset) > rl.window {
 | |
| 		b.count = 1
 | |
| 		b.lastReset = now
 | |
| 		return true
 | |
| 	}
 | |
| 	
 | |
| 	// Check if limit exceeded
 | |
| 	if b.count >= rl.requests {
 | |
| 		return false
 | |
| 	}
 | |
| 	
 | |
| 	// Increment counter
 | |
| 	b.count++
 | |
| 	return true
 | |
| }
 | |
| 
 | |
| // cleanupRoutine periodically removes old buckets
 | |
| func (rl *RateLimiter) cleanupRoutine() {
 | |
| 	ticker := time.NewTicker(rl.cleanup)
 | |
| 	defer ticker.Stop()
 | |
| 	
 | |
| 	for range ticker.C {
 | |
| 		rl.mu.Lock()
 | |
| 		now := time.Now()
 | |
| 		for key, bucket := range rl.buckets {
 | |
| 			if now.Sub(bucket.lastReset) > rl.cleanup {
 | |
| 				delete(rl.buckets, key)
 | |
| 			}
 | |
| 		}
 | |
| 		rl.mu.Unlock()
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // RateLimitMiddleware creates a rate limiting middleware
 | |
| func (rl *RateLimiter) RateLimitMiddleware(next http.Handler) http.Handler {
 | |
| 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | |
| 		// Use IP address as the key
 | |
| 		key := getClientIP(r)
 | |
| 		
 | |
| 		if !rl.Allow(key) {
 | |
| 			log.Warn().
 | |
| 				Str("client_ip", key).
 | |
| 				Str("path", r.URL.Path).
 | |
| 				Msg("Rate limit exceeded")
 | |
| 			
 | |
| 			w.Header().Set("X-RateLimit-Limit", fmt.Sprintf("%d", rl.requests))
 | |
| 			w.Header().Set("X-RateLimit-Window", rl.window.String())
 | |
| 			w.Header().Set("Retry-After", rl.window.String())
 | |
| 			
 | |
| 			http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
 | |
| 			return
 | |
| 		}
 | |
| 		
 | |
| 		next.ServeHTTP(w, r)
 | |
| 	})
 | |
| }
 | |
| 
 | |
| // getClientIP extracts the real client IP address
 | |
| func getClientIP(r *http.Request) string {
 | |
| 	// Check X-Forwarded-For header (when behind proxy)
 | |
| 	xff := r.Header.Get("X-Forwarded-For")
 | |
| 	if xff != "" {
 | |
| 		// Take the first IP in case of multiple
 | |
| 		if idx := len(xff); idx > 0 {
 | |
| 			if commaIdx := 0; commaIdx < idx {
 | |
| 				for i, char := range xff {
 | |
| 					if char == ',' {
 | |
| 						commaIdx = i
 | |
| 						break
 | |
| 					}
 | |
| 				}
 | |
| 				if commaIdx > 0 {
 | |
| 					return xff[:commaIdx]
 | |
| 				}
 | |
| 			}
 | |
| 			return xff
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	// Check X-Real-IP header
 | |
| 	if xri := r.Header.Get("X-Real-IP"); xri != "" {
 | |
| 		return xri
 | |
| 	}
 | |
| 	
 | |
| 	// Fall back to RemoteAddr
 | |
| 	return r.RemoteAddr
 | |
| } |