Fix gRPC health checks and add API Gateway Consul registration
- Fix gRPC health checks: Set serving status for default service (empty string) in all services - Consul checks the default service by default, not specific service names - All services now set both default and specific service status to SERVING - Update Consul registration logic to automatically detect HTTP vs gRPC services - HTTP services (API Gateway) use HTTP health checks - gRPC services use gRPC health checks - Detection based on service tags and metadata - Add API Gateway Consul registration - Register with Docker service name in Docker environment - Use HTTP health checks for API Gateway - Proper host/port configuration handling - Add API Gateway HTTP-to-gRPC handlers - Implement service-specific handlers for Auth and Identity services - Translate HTTP requests to gRPC calls - Map gRPC error codes to HTTP status codes
This commit is contained in:
@@ -58,14 +58,25 @@ func main() {
|
|||||||
}
|
}
|
||||||
gateway.SetupRoutes(srv.Router())
|
gateway.SetupRoutes(srv.Router())
|
||||||
|
|
||||||
// Register with Consul
|
// Determine port and host for registration
|
||||||
gatewayPort := cfg.GetInt("gateway.port")
|
gatewayPort := cfg.GetInt("gateway.port")
|
||||||
if gatewayPort == 0 {
|
if gatewayPort == 0 {
|
||||||
gatewayPort = 8080
|
gatewayPort = cfg.GetInt("server.port")
|
||||||
|
if gatewayPort == 0 {
|
||||||
|
gatewayPort = 8080
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In Docker, always use the Docker service name for health checks
|
||||||
|
// Consul (also in Docker) needs to reach the service via Docker DNS
|
||||||
gatewayHost := cfg.GetString("gateway.host")
|
gatewayHost := cfg.GetString("gateway.host")
|
||||||
if gatewayHost == "" {
|
if os.Getenv("ENVIRONMENT") == "production" || os.Getenv("DOCKER") == "true" {
|
||||||
gatewayHost = "localhost"
|
gatewayHost = "api-gateway" // Docker service name - required for Consul health checks
|
||||||
|
} else if gatewayHost == "" {
|
||||||
|
gatewayHost = cfg.GetString("server.host")
|
||||||
|
if gatewayHost == "" || gatewayHost == "0.0.0.0" {
|
||||||
|
gatewayHost = "localhost" // Local development
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
serviceInstance := ®istry.ServiceInstance{
|
serviceInstance := ®istry.ServiceInstance{
|
||||||
@@ -75,7 +86,8 @@ func main() {
|
|||||||
Port: gatewayPort,
|
Port: gatewayPort,
|
||||||
Tags: []string{"gateway", "http"},
|
Tags: []string{"gateway", "http"},
|
||||||
Metadata: map[string]string{
|
Metadata: map[string]string{
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"protocol": "http",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -324,6 +324,9 @@ func provideAuditService() fx.Option {
|
|||||||
// Register health service
|
// Register health service
|
||||||
healthServer := health.NewServer()
|
healthServer := health.NewServer()
|
||||||
grpc_health_v1.RegisterHealthServer(grpcServer, healthServer)
|
grpc_health_v1.RegisterHealthServer(grpcServer, healthServer)
|
||||||
|
// Set serving status for the default service (empty string) - this is what Consul checks
|
||||||
|
healthServer.SetServingStatus("", grpc_health_v1.HealthCheckResponse_SERVING)
|
||||||
|
// Also set for the specific service name
|
||||||
healthServer.SetServingStatus("audit.v1.AuditService", grpc_health_v1.HealthCheckResponse_SERVING)
|
healthServer.SetServingStatus("audit.v1.AuditService", grpc_health_v1.HealthCheckResponse_SERVING)
|
||||||
|
|
||||||
// Register reflection for grpcurl
|
// Register reflection for grpcurl
|
||||||
|
|||||||
@@ -406,6 +406,9 @@ func provideAuthService() fx.Option {
|
|||||||
// Register health service
|
// Register health service
|
||||||
healthServer := health.NewServer()
|
healthServer := health.NewServer()
|
||||||
grpc_health_v1.RegisterHealthServer(grpcServer, healthServer)
|
grpc_health_v1.RegisterHealthServer(grpcServer, healthServer)
|
||||||
|
// Set serving status for the default service (empty string) - this is what Consul checks
|
||||||
|
healthServer.SetServingStatus("", grpc_health_v1.HealthCheckResponse_SERVING)
|
||||||
|
// Also set for the specific service name
|
||||||
healthServer.SetServingStatus("auth.v1.AuthService", grpc_health_v1.HealthCheckResponse_SERVING)
|
healthServer.SetServingStatus("auth.v1.AuthService", grpc_health_v1.HealthCheckResponse_SERVING)
|
||||||
|
|
||||||
// Register reflection for grpcurl
|
// Register reflection for grpcurl
|
||||||
|
|||||||
@@ -271,6 +271,9 @@ func provideAuthzService() fx.Option {
|
|||||||
// Register health service
|
// Register health service
|
||||||
healthServer := health.NewServer()
|
healthServer := health.NewServer()
|
||||||
grpc_health_v1.RegisterHealthServer(grpcServer, healthServer)
|
grpc_health_v1.RegisterHealthServer(grpcServer, healthServer)
|
||||||
|
// Set serving status for the default service (empty string) - this is what Consul checks
|
||||||
|
healthServer.SetServingStatus("", grpc_health_v1.HealthCheckResponse_SERVING)
|
||||||
|
// Also set for the specific service name
|
||||||
healthServer.SetServingStatus("authz.v1.AuthzService", grpc_health_v1.HealthCheckResponse_SERVING)
|
healthServer.SetServingStatus("authz.v1.AuthzService", grpc_health_v1.HealthCheckResponse_SERVING)
|
||||||
|
|
||||||
// Register reflection for grpcurl
|
// Register reflection for grpcurl
|
||||||
|
|||||||
@@ -418,6 +418,9 @@ func provideIdentityService() fx.Option {
|
|||||||
// Register health service
|
// Register health service
|
||||||
healthServer := health.NewServer()
|
healthServer := health.NewServer()
|
||||||
grpc_health_v1.RegisterHealthServer(grpcServer, healthServer)
|
grpc_health_v1.RegisterHealthServer(grpcServer, healthServer)
|
||||||
|
// Set serving status for the default service (empty string) - this is what Consul checks
|
||||||
|
healthServer.SetServingStatus("", grpc_health_v1.HealthCheckResponse_SERVING)
|
||||||
|
// Also set for the specific service name
|
||||||
healthServer.SetServingStatus("identity.v1.IdentityService", grpc_health_v1.HealthCheckResponse_SERVING)
|
healthServer.SetServingStatus("identity.v1.IdentityService", grpc_health_v1.HealthCheckResponse_SERVING)
|
||||||
|
|
||||||
// Register reflection for grpcurl
|
// Register reflection for grpcurl
|
||||||
|
|||||||
@@ -69,12 +69,42 @@ func (r *ConsulRegistry) Register(ctx context.Context, service *registry.Service
|
|||||||
Meta: service.Metadata,
|
Meta: service.Metadata,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine health check type based on service metadata/tags or config
|
||||||
|
// Check if service is HTTP (has "http" tag or protocol metadata)
|
||||||
|
isHTTP := false
|
||||||
|
for _, tag := range service.Tags {
|
||||||
|
if tag == "http" {
|
||||||
|
isHTTP = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !isHTTP && service.Metadata != nil {
|
||||||
|
if protocol, ok := service.Metadata["protocol"]; ok && protocol == "http" {
|
||||||
|
isHTTP = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Add health check if configured
|
// Add health check if configured
|
||||||
if r.config.HealthCheck.UseGRPC {
|
if isHTTP && r.config.HealthCheck.HTTP != "" {
|
||||||
|
// Use HTTP health check for HTTP services (e.g., API Gateway)
|
||||||
|
healthCheckURL := fmt.Sprintf("http://%s:%d%s", service.Address, service.Port, r.config.HealthCheck.HTTP)
|
||||||
|
registration.Check = &consulapi.AgentServiceCheck{
|
||||||
|
HTTP: healthCheckURL,
|
||||||
|
Interval: r.config.HealthCheck.Interval.String(),
|
||||||
|
Timeout: r.config.HealthCheck.Timeout.String(),
|
||||||
|
DeregisterCriticalServiceAfter: r.config.HealthCheck.DeregisterAfter.String(),
|
||||||
|
}
|
||||||
|
} else if !isHTTP && r.config.HealthCheck.UseGRPC {
|
||||||
// Use gRPC health check for gRPC services
|
// Use gRPC health check for gRPC services
|
||||||
// Format: host:port/service or host:port (uses default health service)
|
// Format: host:port (checks default service with empty string name)
|
||||||
|
// Or: host:port/service (checks specific service name)
|
||||||
|
// We use host:port to check the default service (empty string)
|
||||||
grpcAddr := fmt.Sprintf("%s:%d", service.Address, service.Port)
|
grpcAddr := fmt.Sprintf("%s:%d", service.Address, service.Port)
|
||||||
if r.config.HealthCheck.GRPC != "" {
|
// If a specific service name is provided, append it
|
||||||
|
// Otherwise, check the default service (empty string) which we set in each service
|
||||||
|
if r.config.HealthCheck.GRPC != "" && r.config.HealthCheck.GRPC != "grpc.health.v1.Health" {
|
||||||
|
// Only append if it's not the default health service name
|
||||||
|
// The GRPC field in Consul expects the application service name, not the proto service name
|
||||||
grpcAddr = fmt.Sprintf("%s:%d/%s", service.Address, service.Port, r.config.HealthCheck.GRPC)
|
grpcAddr = fmt.Sprintf("%s:%d/%s", service.Address, service.Port, r.config.HealthCheck.GRPC)
|
||||||
}
|
}
|
||||||
registration.Check = &consulapi.AgentServiceCheck{
|
registration.Check = &consulapi.AgentServiceCheck{
|
||||||
@@ -84,7 +114,7 @@ func (r *ConsulRegistry) Register(ctx context.Context, service *registry.Service
|
|||||||
DeregisterCriticalServiceAfter: r.config.HealthCheck.DeregisterAfter.String(),
|
DeregisterCriticalServiceAfter: r.config.HealthCheck.DeregisterAfter.String(),
|
||||||
}
|
}
|
||||||
} else if r.config.HealthCheck.HTTP != "" {
|
} else if r.config.HealthCheck.HTTP != "" {
|
||||||
// Use HTTP health check for HTTP services
|
// Fallback to HTTP if HTTP endpoint is configured and service is not explicitly gRPC
|
||||||
healthCheckURL := fmt.Sprintf("http://%s:%d%s", service.Address, service.Port, r.config.HealthCheck.HTTP)
|
healthCheckURL := fmt.Sprintf("http://%s:%d%s", service.Address, service.Port, r.config.HealthCheck.HTTP)
|
||||||
registration.Check = &consulapi.AgentServiceCheck{
|
registration.Check = &consulapi.AgentServiceCheck{
|
||||||
HTTP: healthCheckURL,
|
HTTP: healthCheckURL,
|
||||||
|
|||||||
@@ -2,10 +2,8 @@
|
|||||||
package gateway
|
package gateway
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"strings"
|
||||||
"net/url"
|
|
||||||
|
|
||||||
"git.dcentral.systems/toolz/goplt/internal/client"
|
"git.dcentral.systems/toolz/goplt/internal/client"
|
||||||
"git.dcentral.systems/toolz/goplt/pkg/config"
|
"git.dcentral.systems/toolz/goplt/pkg/config"
|
||||||
@@ -51,10 +49,50 @@ func NewGateway(
|
|||||||
|
|
||||||
// SetupRoutes configures routes on the Gin router.
|
// SetupRoutes configures routes on the Gin router.
|
||||||
func (g *Gateway) SetupRoutes(router *gin.Engine) {
|
func (g *Gateway) SetupRoutes(router *gin.Engine) {
|
||||||
// Setup route handlers
|
// Register routes with wildcard support
|
||||||
|
// Gin uses /*path for wildcards, so we convert /** to /*path
|
||||||
for _, route := range g.routes {
|
for _, route := range g.routes {
|
||||||
route := route // Capture for closure
|
route := route // Capture for closure
|
||||||
router.Any(route.Path, g.handleRoute(route))
|
|
||||||
|
// 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
|
// Default handler for unmatched routes
|
||||||
@@ -66,62 +104,108 @@ func (g *Gateway) SetupRoutes(router *gin.Engine) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleRoute returns a handler function for a route.
|
// matchRoute finds the matching route configuration for a given path.
|
||||||
func (g *Gateway) handleRoute(route RouteConfig) gin.HandlerFunc {
|
func (g *Gateway) matchRoute(path string) *RouteConfig {
|
||||||
return func(c *gin.Context) {
|
for _, route := range g.routes {
|
||||||
// TODO: Add authentication middleware if auth_required is true
|
if g.pathMatches(path, route.Path) {
|
||||||
// TODO: Add rate limiting middleware
|
return &route
|
||||||
// TODO: Add CORS middleware
|
|
||||||
|
|
||||||
// Discover service instances
|
|
||||||
ctx := c.Request.Context()
|
|
||||||
instances, err := g.registry.Discover(ctx, route.Service)
|
|
||||||
if err != nil {
|
|
||||||
g.log.Error("Failed to discover service",
|
|
||||||
logger.String("service", route.Service),
|
|
||||||
logger.Error(err),
|
|
||||||
)
|
|
||||||
c.JSON(http.StatusServiceUnavailable, gin.H{
|
|
||||||
"error": "Service unavailable",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(instances) == 0 {
|
|
||||||
g.log.Warn("No instances found for service",
|
|
||||||
logger.String("service", route.Service),
|
|
||||||
)
|
|
||||||
c.JSON(http.StatusServiceUnavailable, gin.H{
|
|
||||||
"error": "Service unavailable",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use first healthy instance (load balancing can be added later)
|
|
||||||
instance := instances[0]
|
|
||||||
targetURL := fmt.Sprintf("http://%s:%d", instance.Address, instance.Port)
|
|
||||||
|
|
||||||
// Create reverse proxy
|
|
||||||
target, err := url.Parse(targetURL)
|
|
||||||
if err != nil {
|
|
||||||
g.log.Error("Failed to parse target URL",
|
|
||||||
logger.String("url", targetURL),
|
|
||||||
logger.Error(err),
|
|
||||||
)
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
|
||||||
"error": "Internal server error",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
proxy := httputil.NewSingleHostReverseProxy(target)
|
|
||||||
proxy.ServeHTTP(c.Writer, c.Request)
|
|
||||||
}
|
}
|
||||||
|
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.
|
// loadRoutes loads route configurations from config.
|
||||||
func loadRoutes(cfg config.ConfigProvider) []RouteConfig {
|
func loadRoutes(cfg config.ConfigProvider) []RouteConfig {
|
||||||
// For now, return empty routes - will be loaded from config in future
|
if cfg == nil {
|
||||||
// This is a placeholder implementation
|
return []RouteConfig{}
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
406
services/gateway/handlers.go
Normal file
406
services/gateway/handlers.go
Normal file
@@ -0,0 +1,406 @@
|
|||||||
|
// Package gateway provides API Gateway implementation.
|
||||||
|
package gateway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.dcentral.systems/toolz/goplt/pkg/logger"
|
||||||
|
"git.dcentral.systems/toolz/goplt/pkg/services"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
// handleAuthService handles requests for auth-service routes.
|
||||||
|
func (g *Gateway) handleAuthService(c *gin.Context, route RouteConfig, remainingPath string) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
// Get auth client
|
||||||
|
authClient, err := g.clientFactory.GetAuthClient()
|
||||||
|
if err != nil {
|
||||||
|
g.log.Error("Failed to get auth client",
|
||||||
|
logger.String("error", err.Error()),
|
||||||
|
)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
|
"error": "Internal server error",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route based on path and method
|
||||||
|
switch {
|
||||||
|
case c.Request.Method == http.MethodPost && remainingPath == "/login":
|
||||||
|
g.handleLogin(ctx, c, authClient)
|
||||||
|
case c.Request.Method == http.MethodPost && remainingPath == "/refresh":
|
||||||
|
g.handleRefreshToken(ctx, c, authClient)
|
||||||
|
case c.Request.Method == http.MethodPost && remainingPath == "/validate":
|
||||||
|
g.handleValidateToken(ctx, c, authClient)
|
||||||
|
case c.Request.Method == http.MethodPost && remainingPath == "/logout":
|
||||||
|
g.handleLogout(ctx, c, authClient)
|
||||||
|
default:
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{
|
||||||
|
"error": "Endpoint not found",
|
||||||
|
"path": remainingPath,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleIdentityService handles requests for identity-service routes.
|
||||||
|
func (g *Gateway) handleIdentityService(c *gin.Context, route RouteConfig, remainingPath string) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
// Get identity client
|
||||||
|
identityClient, err := g.clientFactory.GetIdentityClient()
|
||||||
|
if err != nil {
|
||||||
|
g.log.Error("Failed to get identity client",
|
||||||
|
logger.String("error", err.Error()),
|
||||||
|
)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
|
"error": "Internal server error",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route based on path and method
|
||||||
|
pathParts := strings.Split(strings.Trim(remainingPath, "/"), "/")
|
||||||
|
|
||||||
|
switch {
|
||||||
|
// GET /api/v1/users/:id
|
||||||
|
case c.Request.Method == http.MethodGet && len(pathParts) == 1 && pathParts[0] != "":
|
||||||
|
userID := pathParts[0]
|
||||||
|
g.handleGetUser(ctx, c, identityClient, userID)
|
||||||
|
|
||||||
|
// GET /api/v1/users?email=...
|
||||||
|
case c.Request.Method == http.MethodGet && remainingPath == "" && c.Query("email") != "":
|
||||||
|
email := c.Query("email")
|
||||||
|
g.handleGetUserByEmail(ctx, c, identityClient, email)
|
||||||
|
|
||||||
|
// POST /api/v1/users
|
||||||
|
case c.Request.Method == http.MethodPost && remainingPath == "":
|
||||||
|
g.handleCreateUser(ctx, c, identityClient)
|
||||||
|
|
||||||
|
// PUT /api/v1/users/:id
|
||||||
|
case c.Request.Method == http.MethodPut && len(pathParts) == 1 && pathParts[0] != "":
|
||||||
|
userID := pathParts[0]
|
||||||
|
g.handleUpdateUser(ctx, c, identityClient, userID)
|
||||||
|
|
||||||
|
// DELETE /api/v1/users/:id
|
||||||
|
case c.Request.Method == http.MethodDelete && len(pathParts) == 1 && pathParts[0] != "":
|
||||||
|
userID := pathParts[0]
|
||||||
|
g.handleDeleteUser(ctx, c, identityClient, userID)
|
||||||
|
|
||||||
|
// POST /api/v1/users/verify-email
|
||||||
|
case c.Request.Method == http.MethodPost && remainingPath == "/verify-email":
|
||||||
|
g.handleVerifyEmail(ctx, c, identityClient)
|
||||||
|
|
||||||
|
// POST /api/v1/users/request-password-reset
|
||||||
|
case c.Request.Method == http.MethodPost && remainingPath == "/request-password-reset":
|
||||||
|
g.handleRequestPasswordReset(ctx, c, identityClient)
|
||||||
|
|
||||||
|
// POST /api/v1/users/reset-password
|
||||||
|
case c.Request.Method == http.MethodPost && remainingPath == "/reset-password":
|
||||||
|
g.handleResetPassword(ctx, c, identityClient)
|
||||||
|
|
||||||
|
default:
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{
|
||||||
|
"error": "Endpoint not found",
|
||||||
|
"path": remainingPath,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auth Service Handlers
|
||||||
|
|
||||||
|
func (g *Gateway) handleLogin(ctx context.Context, c *gin.Context, client services.AuthServiceClient) {
|
||||||
|
var req struct {
|
||||||
|
Email string `json:"email" binding:"required"`
|
||||||
|
Password string `json:"password" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"error": "Invalid request",
|
||||||
|
"details": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenResp, err := client.Login(ctx, req.Email, req.Password)
|
||||||
|
if err != nil {
|
||||||
|
g.handleGRPCError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, tokenResp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Gateway) handleRefreshToken(ctx context.Context, c *gin.Context, client services.AuthServiceClient) {
|
||||||
|
var req struct {
|
||||||
|
RefreshToken string `json:"refresh_token" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"error": "Invalid request",
|
||||||
|
"details": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenResp, err := client.RefreshToken(ctx, req.RefreshToken)
|
||||||
|
if err != nil {
|
||||||
|
g.handleGRPCError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, tokenResp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Gateway) handleValidateToken(ctx context.Context, c *gin.Context, client services.AuthServiceClient) {
|
||||||
|
var req struct {
|
||||||
|
Token string `json:"token" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"error": "Invalid request",
|
||||||
|
"details": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, err := client.ValidateToken(ctx, req.Token)
|
||||||
|
if err != nil {
|
||||||
|
g.handleGRPCError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, claims)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Gateway) handleLogout(ctx context.Context, c *gin.Context, client services.AuthServiceClient) {
|
||||||
|
var req struct {
|
||||||
|
RefreshToken string `json:"refresh_token" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"error": "Invalid request",
|
||||||
|
"details": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := client.Logout(ctx, req.RefreshToken)
|
||||||
|
if err != nil {
|
||||||
|
g.handleGRPCError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Identity Service Handlers
|
||||||
|
|
||||||
|
func (g *Gateway) handleGetUser(ctx context.Context, c *gin.Context, client services.IdentityServiceClient, userID string) {
|
||||||
|
user, err := client.GetUser(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
g.handleGRPCError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Gateway) handleGetUserByEmail(ctx context.Context, c *gin.Context, client services.IdentityServiceClient, email string) {
|
||||||
|
user, err := client.GetUserByEmail(ctx, email)
|
||||||
|
if err != nil {
|
||||||
|
g.handleGRPCError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Gateway) handleCreateUser(ctx context.Context, c *gin.Context, client services.IdentityServiceClient) {
|
||||||
|
var req services.CreateUserRequest
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"error": "Invalid request",
|
||||||
|
"details": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := client.CreateUser(ctx, &req)
|
||||||
|
if err != nil {
|
||||||
|
g.handleGRPCError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusCreated, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Gateway) handleUpdateUser(ctx context.Context, c *gin.Context, client services.IdentityServiceClient, userID string) {
|
||||||
|
var req services.UpdateUserRequest
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"error": "Invalid request",
|
||||||
|
"details": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := client.UpdateUser(ctx, userID, &req)
|
||||||
|
if err != nil {
|
||||||
|
g.handleGRPCError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Gateway) handleDeleteUser(ctx context.Context, c *gin.Context, client services.IdentityServiceClient, userID string) {
|
||||||
|
err := client.DeleteUser(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
g.handleGRPCError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Gateway) handleVerifyEmail(ctx context.Context, c *gin.Context, client services.IdentityServiceClient) {
|
||||||
|
var req struct {
|
||||||
|
Token string `json:"token" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"error": "Invalid request",
|
||||||
|
"details": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := client.VerifyEmail(ctx, req.Token)
|
||||||
|
if err != nil {
|
||||||
|
g.handleGRPCError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Gateway) handleRequestPasswordReset(ctx context.Context, c *gin.Context, client services.IdentityServiceClient) {
|
||||||
|
var req struct {
|
||||||
|
Email string `json:"email" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"error": "Invalid request",
|
||||||
|
"details": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := client.RequestPasswordReset(ctx, req.Email)
|
||||||
|
if err != nil {
|
||||||
|
g.handleGRPCError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Gateway) handleResetPassword(ctx context.Context, c *gin.Context, client services.IdentityServiceClient) {
|
||||||
|
var req struct {
|
||||||
|
Token string `json:"token" binding:"required"`
|
||||||
|
NewPassword string `json:"new_password" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"error": "Invalid request",
|
||||||
|
"details": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := client.ResetPassword(ctx, req.Token, req.NewPassword)
|
||||||
|
if err != nil {
|
||||||
|
g.handleGRPCError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleGRPCError converts gRPC errors to HTTP status codes and responses.
|
||||||
|
func (g *Gateway) handleGRPCError(c *gin.Context, err error) {
|
||||||
|
st, ok := status.FromError(err)
|
||||||
|
if !ok {
|
||||||
|
g.log.Error("Non-gRPC error from service",
|
||||||
|
logger.String("error", err.Error()),
|
||||||
|
)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
|
"error": "Internal server error",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var httpStatus int
|
||||||
|
var errorMsg string
|
||||||
|
|
||||||
|
switch st.Code() {
|
||||||
|
case codes.Unauthenticated:
|
||||||
|
httpStatus = http.StatusUnauthorized
|
||||||
|
errorMsg = "Unauthorized"
|
||||||
|
case codes.PermissionDenied:
|
||||||
|
httpStatus = http.StatusForbidden
|
||||||
|
errorMsg = "Forbidden"
|
||||||
|
case codes.NotFound:
|
||||||
|
httpStatus = http.StatusNotFound
|
||||||
|
errorMsg = "Not found"
|
||||||
|
case codes.InvalidArgument:
|
||||||
|
httpStatus = http.StatusBadRequest
|
||||||
|
errorMsg = "Invalid request"
|
||||||
|
case codes.AlreadyExists:
|
||||||
|
httpStatus = http.StatusConflict
|
||||||
|
errorMsg = "Resource already exists"
|
||||||
|
case codes.Internal:
|
||||||
|
httpStatus = http.StatusInternalServerError
|
||||||
|
errorMsg = "Internal server error"
|
||||||
|
case codes.Unavailable:
|
||||||
|
httpStatus = http.StatusServiceUnavailable
|
||||||
|
errorMsg = "Service unavailable"
|
||||||
|
default:
|
||||||
|
httpStatus = http.StatusInternalServerError
|
||||||
|
errorMsg = "Internal server error"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include gRPC error message if available
|
||||||
|
response := gin.H{
|
||||||
|
"error": errorMsg,
|
||||||
|
}
|
||||||
|
if st.Message() != "" {
|
||||||
|
response["details"] = st.Message()
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(httpStatus, response)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user