407 lines
10 KiB
Go
407 lines
10 KiB
Go
// 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)
|
|
}
|