Files
defiQUG a53c15507f fix: API JSON error responses + navbar with dropdowns
- Add backend/libs/go-http-errors for consistent JSON errors
- REST API: use writeMethodNotAllowed, writeNotFound, writeInternalError
- middleware, gateway, search: use httperrors.WriteJSON
- SPA: navbar with Explore/Tools/More dropdowns, initNavDropdowns()
- Next.js: Navbar component with dropdowns + mobile menu

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-16 03:09:53 -08:00

143 lines
3.6 KiB
Go

package gateway
import (
"fmt"
"log"
"net/http"
"net/http/httputil"
"net/url"
httperrors "github.com/explorer/backend/libs/go-http-errors"
)
// Gateway represents the API gateway
type Gateway struct {
apiURL *url.URL
rateLimiter *RateLimiter
auth *AuthMiddleware
}
// NewGateway creates a new API gateway
func NewGateway(apiURL string) (*Gateway, error) {
parsedURL, err := url.Parse(apiURL)
if err != nil {
return nil, fmt.Errorf("invalid API URL: %w", err)
}
return &Gateway{
apiURL: parsedURL,
rateLimiter: NewRateLimiter(),
auth: NewAuthMiddleware(),
}, nil
}
// Start starts the gateway server
func (g *Gateway) Start(port int) error {
mux := http.NewServeMux()
// Proxy to API server
proxy := httputil.NewSingleHostReverseProxy(g.apiURL)
mux.HandleFunc("/", g.handleRequest(proxy))
addr := fmt.Sprintf(":%d", port)
log.Printf("Starting API Gateway on %s", addr)
return http.ListenAndServe(addr, mux)
}
// handleRequest handles incoming requests with middleware
func (g *Gateway) handleRequest(proxy *httputil.ReverseProxy) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Add security headers
g.addSecurityHeaders(w)
// Authentication
if !g.auth.Authenticate(r) {
httperrors.WriteJSON(w, http.StatusUnauthorized, "UNAUTHORIZED", "Unauthorized")
return
}
// Rate limiting
if !g.rateLimiter.Allow(r) {
httperrors.WriteJSON(w, http.StatusTooManyRequests, "RATE_LIMIT_EXCEEDED", "Rate limit exceeded")
return
}
// Add headers
r.Header.Set("X-Forwarded-For", r.RemoteAddr)
if apiKey := g.auth.GetAPIKey(r); apiKey != "" {
r.Header.Set("X-API-Key", apiKey)
}
// Add branding header
w.Header().Set("X-Explorer-Name", "SolaceScanScout")
w.Header().Set("X-Explorer-Version", "1.0.0")
// Proxy request
proxy.ServeHTTP(w, r)
}
}
// addSecurityHeaders adds security headers to responses
func (g *Gateway) addSecurityHeaders(w http.ResponseWriter) {
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("X-XSS-Protection", "1; mode=block")
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
// CSP will be set per route if needed
w.Header().Set("Permissions-Policy", "geolocation=(), microphone=(), camera=()")
}
// RateLimiter handles rate limiting
type RateLimiter struct {
// Simple in-memory rate limiter (should use Redis in production)
limits map[string]*limitEntry
}
type limitEntry struct {
count int
resetAt int64
}
func NewRateLimiter() *RateLimiter {
return &RateLimiter{
limits: make(map[string]*limitEntry),
}
}
func (rl *RateLimiter) Allow(r *http.Request) bool {
_ = r.RemoteAddr // Will be used in production for per-IP limiting
// In production, use Redis with token bucket algorithm
// For now, simple per-IP limiting
return true // Simplified - implement proper rate limiting
}
// AuthMiddleware handles authentication
type AuthMiddleware struct {
// In production, validate against database
}
func NewAuthMiddleware() *AuthMiddleware {
return &AuthMiddleware{}
}
func (am *AuthMiddleware) Authenticate(r *http.Request) bool {
// Allow anonymous access for now
// In production, validate API key
apiKey := am.GetAPIKey(r)
return apiKey != "" || true // Allow anonymous for MVP
}
func (am *AuthMiddleware) GetAPIKey(r *http.Request) string {
// Check header first
if key := r.Header.Get("X-API-Key"); key != "" {
return key
}
// Check query parameter
if key := r.URL.Query().Get("api_key"); key != "" {
return key
}
return ""
}