212 lines
5.3 KiB
Go
212 lines
5.3 KiB
Go
// Package gateway provides API Gateway implementation.
|
|
package gateway
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
|
|
"git.dcentral.systems/toolz/goplt/internal/client"
|
|
"git.dcentral.systems/toolz/goplt/pkg/config"
|
|
"git.dcentral.systems/toolz/goplt/pkg/logger"
|
|
"git.dcentral.systems/toolz/goplt/pkg/registry"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// Gateway handles routing requests to backend services.
|
|
type Gateway struct {
|
|
config config.ConfigProvider
|
|
log logger.Logger
|
|
clientFactory *client.ServiceClientFactory
|
|
registry registry.ServiceRegistry
|
|
routes []RouteConfig
|
|
}
|
|
|
|
// RouteConfig defines a route configuration.
|
|
type RouteConfig struct {
|
|
Path string
|
|
Service string
|
|
AuthRequired bool
|
|
}
|
|
|
|
// NewGateway creates a new API Gateway instance.
|
|
func NewGateway(
|
|
cfg config.ConfigProvider,
|
|
log logger.Logger,
|
|
clientFactory *client.ServiceClientFactory,
|
|
reg registry.ServiceRegistry,
|
|
) (*Gateway, error) {
|
|
// Load route configurations
|
|
routes := loadRoutes(cfg)
|
|
|
|
return &Gateway{
|
|
config: cfg,
|
|
log: log,
|
|
clientFactory: clientFactory,
|
|
registry: reg,
|
|
routes: routes,
|
|
}, nil
|
|
}
|
|
|
|
// SetupRoutes configures routes on the Gin router.
|
|
func (g *Gateway) SetupRoutes(router *gin.Engine) {
|
|
// Register routes with wildcard support
|
|
// Gin uses /*path for wildcards, so we convert /** to /*path
|
|
for _, route := range g.routes {
|
|
route := route // Capture for closure
|
|
|
|
// Convert /** wildcard to Gin's /*path format
|
|
ginPath := route.Path
|
|
hasWildcard := strings.HasSuffix(ginPath, "/**")
|
|
if hasWildcard {
|
|
ginPath = strings.TrimSuffix(ginPath, "/**") + "/*path"
|
|
}
|
|
|
|
// Register all HTTP methods for this route
|
|
router.Any(ginPath, func(c *gin.Context) {
|
|
// Extract the remaining path
|
|
var remainingPath string
|
|
if hasWildcard {
|
|
// Extract from Gin's path parameter
|
|
pathParam := c.Param("path")
|
|
if pathParam != "" {
|
|
remainingPath = "/" + pathParam
|
|
} else {
|
|
remainingPath = "/"
|
|
}
|
|
} else {
|
|
// Exact match - no remaining path
|
|
remainingPath = "/"
|
|
}
|
|
|
|
// Route to appropriate service handler
|
|
switch route.Service {
|
|
case "auth-service":
|
|
g.handleAuthService(c, route, remainingPath)
|
|
case "identity-service":
|
|
g.handleIdentityService(c, route, remainingPath)
|
|
default:
|
|
g.log.Warn("Unknown service",
|
|
logger.String("service", route.Service),
|
|
)
|
|
c.JSON(http.StatusNotFound, gin.H{
|
|
"error": "Service not found",
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
// Default handler for unmatched routes
|
|
router.NoRoute(func(c *gin.Context) {
|
|
c.JSON(http.StatusNotFound, gin.H{
|
|
"error": "Route not found",
|
|
"path": c.Request.URL.Path,
|
|
})
|
|
})
|
|
}
|
|
|
|
// matchRoute finds the matching route configuration for a given path.
|
|
func (g *Gateway) matchRoute(path string) *RouteConfig {
|
|
for _, route := range g.routes {
|
|
if g.pathMatches(path, route.Path) {
|
|
return &route
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// pathMatches checks if a request path matches a route pattern.
|
|
// Supports wildcard matching: "/api/v1/auth/**" matches "/api/v1/auth/login", etc.
|
|
func (g *Gateway) pathMatches(requestPath, routePath string) bool {
|
|
// Remove trailing slashes for comparison
|
|
requestPath = strings.TrimSuffix(requestPath, "/")
|
|
routePath = strings.TrimSuffix(routePath, "/")
|
|
|
|
// Exact match
|
|
if requestPath == routePath {
|
|
return true
|
|
}
|
|
|
|
// Wildcard match: routePath ends with "/**"
|
|
if strings.HasSuffix(routePath, "/**") {
|
|
prefix := strings.TrimSuffix(routePath, "/**")
|
|
return strings.HasPrefix(requestPath, prefix+"/") || requestPath == prefix
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// extractRemainingPath extracts the path segment after the route prefix.
|
|
// Example: path="/api/v1/auth/login", route="/api/v1/auth/**" -> returns "/login"
|
|
func (g *Gateway) extractRemainingPath(requestPath, routePath string) string {
|
|
// Remove trailing slashes
|
|
requestPath = strings.TrimSuffix(requestPath, "/")
|
|
routePath = strings.TrimSuffix(routePath, "/")
|
|
|
|
// Handle wildcard routes
|
|
if strings.HasSuffix(routePath, "/**") {
|
|
prefix := strings.TrimSuffix(routePath, "/**")
|
|
if strings.HasPrefix(requestPath, prefix) {
|
|
remaining := strings.TrimPrefix(requestPath, prefix)
|
|
if remaining == "" {
|
|
return "/"
|
|
}
|
|
return remaining
|
|
}
|
|
}
|
|
|
|
// Exact match - no remaining path
|
|
if requestPath == routePath {
|
|
return "/"
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// loadRoutes loads route configurations from config.
|
|
func loadRoutes(cfg config.ConfigProvider) []RouteConfig {
|
|
if cfg == nil {
|
|
return []RouteConfig{}
|
|
}
|
|
|
|
// Get routes from config
|
|
routesInterface := cfg.Get("gateway.routes")
|
|
if routesInterface == nil {
|
|
return []RouteConfig{}
|
|
}
|
|
|
|
// Convert to slice of RouteConfig
|
|
routesSlice, ok := routesInterface.([]interface{})
|
|
if !ok {
|
|
return []RouteConfig{}
|
|
}
|
|
|
|
routes := make([]RouteConfig, 0, len(routesSlice))
|
|
for _, routeInterface := range routesSlice {
|
|
routeMap, ok := routeInterface.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
route := RouteConfig{}
|
|
|
|
if path, ok := routeMap["path"].(string); ok {
|
|
route.Path = path
|
|
}
|
|
|
|
if service, ok := routeMap["service"].(string); ok {
|
|
route.Service = service
|
|
}
|
|
|
|
if authRequired, ok := routeMap["auth_required"].(bool); ok {
|
|
route.AuthRequired = authRequired
|
|
}
|
|
|
|
// Only add route if it has required fields
|
|
if route.Path != "" && route.Service != "" {
|
|
routes = append(routes, route)
|
|
}
|
|
}
|
|
|
|
return routes
|
|
}
|