// 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) }