Add full monorepo: virtual-banker, backend, frontend, docs, scripts, deployment
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
32
backend/api/gateway/cmd/main.go
Normal file
32
backend/api/gateway/cmd/main.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/explorer/backend/api/gateway"
|
||||
)
|
||||
|
||||
func main() {
|
||||
apiURL := os.Getenv("API_URL")
|
||||
if apiURL == "" {
|
||||
apiURL = "http://localhost:8080"
|
||||
}
|
||||
|
||||
gw, err := gateway.NewGateway(apiURL)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create gateway: %v", err)
|
||||
}
|
||||
|
||||
port := 8081
|
||||
if envPort := os.Getenv("GATEWAY_PORT"); envPort != "" {
|
||||
if p, err := strconv.Atoi(envPort); err == nil {
|
||||
port = p
|
||||
}
|
||||
}
|
||||
|
||||
if err := gw.Start(port); err != nil {
|
||||
log.Fatalf("Failed to start gateway: %v", err)
|
||||
}
|
||||
}
|
||||
140
backend/api/gateway/gateway.go
Normal file
140
backend/api/gateway/gateway.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// 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) {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Rate limiting
|
||||
if !g.rateLimiter.Allow(r) {
|
||||
http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
|
||||
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 ""
|
||||
}
|
||||
Reference in New Issue
Block a user