From 8285cf03844efef86df88914eadd3f8ec340e4e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Z=C3=BCrcher?= Date: Fri, 24 Oct 2025 16:29:37 +0200 Subject: [PATCH] add middleware for api --- api.go | 9 +-- db_test.go | 2 +- middleware.go | 2 +- middlewareApi.go | 167 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 170 insertions(+), 10 deletions(-) create mode 100644 middlewareApi.go diff --git a/api.go b/api.go index a1e3cec..5a93280 100644 --- a/api.go +++ b/api.go @@ -34,13 +34,6 @@ func (aH *AccessHandlerAPI) AddUser(c *gin.Context) { return } - hash, err := utils.HashPassword(user.Password) - if err != nil { - aH.logger.Error("AddUser", err) - c.JSON(http.StatusInternalServerError, nil) - return - } - if !utils.IsValidEmail(user.Email) { aH.logger.Error("AddUser", "not valid email address") c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(errors.New("not valid email address"))) @@ -48,7 +41,7 @@ func (aH *AccessHandlerAPI) AddUser(c *gin.Context) { } // Hash the provided password before saving - hash, err = utils.HashPassword(user.Password) + hash, err := utils.HashPassword(user.Password) if err != nil { aH.logger.Error("AddUser", err) c.JSON(http.StatusInternalServerError, nil) diff --git a/db_test.go b/db_test.go index c37ab52..2a1f68b 100644 --- a/db_test.go +++ b/db_test.go @@ -112,7 +112,7 @@ func TestLoginHandler(t *testing.T) { r.POST("/login/refresh", aH.Refresh) r.GET("/login/me", aH.Me) r.GET("/logout", aH.Logout) - middleware := r.Group("", AuthMiddleware()) + middleware := r.Group("", aH.AuthMiddleware()) auth := middleware.Group("/members", aH.AuthorizeRole("")) auth.GET("", func(ctx *gin.Context) { diff --git a/middleware.go b/middleware.go index 28495c5..ab1ef23 100644 --- a/middleware.go +++ b/middleware.go @@ -56,7 +56,7 @@ func SetMiddlewareLogger(r *gin.Engine, logger *logging.Logger) { // Usage: // // r.Use(AuthMiddleware()) -func AuthMiddleware() gin.HandlerFunc { +func (aH *AccessHandler) AuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { // Retrieve logger from Gin context middlewareLogger, ok := c.Get("logger") diff --git a/middlewareApi.go b/middlewareApi.go new file mode 100644 index 0000000..2343f88 --- /dev/null +++ b/middlewareApi.go @@ -0,0 +1,167 @@ +package AccessHandler + +import ( + "fmt" + "log" + "net/http" + "strings" + + "gitea.tecamino.com/paadi/access-handler/models" + "gitea.tecamino.com/paadi/tecamino-logger/logging" + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v5" +) + +// SetMiddlewareLogger +// +// Description: +// +// Registers a Gin middleware that attaches a custom logger instance +// to every incoming request via the Gin context. This allows other +// middleware and handlers to access the logger using: +// +// logger := c.MustGet("logger").(*logging.Logger) +// +// Parameters: +// - r: The Gin engine to which the middleware is applied. +// - logger: A pointer to the application's custom logger. +// +// Usage: +// +// SetMiddlewareLogger(router, logger) +func (aH *AccessHandlerAPI) SetMiddlewareLogger(r *gin.Engine, logger *logging.Logger) { + // Add middleware that injects logger into context + r.Use(func(c *gin.Context) { + c.Set("logger", logger) + c.Next() + }) +} + +// AuthMiddleware +// +// Description: +// +// A Gin middleware that performs authentication using a JWT token stored +// in a cookie named "access_token". It validates the token and extracts +// the user's role from the claims, storing it in the Gin context. +// +// Behavior: +// - Requires that SetMiddlewareLogger was used earlier to inject the logger. +// - If the JWT cookie is missing or invalid, it aborts the request with +// an appropriate HTTP error (401 or 500). +// +// Returns: +// +// A Gin handler function that can be used as middleware. +// +// Usage: +// +// r.Use(AuthMiddleware()) +func (aH *AccessHandlerAPI) AuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + // Retrieve logger from Gin context + middlewareLogger, ok := c.Get("logger") + if !ok { + log.Fatal("middleware logger not set — use SetMiddlewareLogger first") + c.AbortWithStatusJSON(http.StatusInternalServerError, http.StatusInternalServerError) + return + } + logger := middlewareLogger.(*logging.Logger) + + // Read access token from cookie + cookie, err := c.Cookie("access_token") + if err != nil { + logger.Error("AuthMiddleware", err) + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "not logged in"}) + return + } + + // Parse and validate JWT token + token, err := jwt.Parse(cookie, func(t *jwt.Token) (any, error) { + return ACCESS_SECRET, nil + }) + if err != nil || !token.Valid { + logger.Error("AuthMiddleware", err) + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "invalid token"}) + return + } + + // Extract custom claims (role) + if claims, ok := token.Claims.(jwt.MapClaims); ok { + role, _ := claims["role"].(string) + c.Set("role", role) + } + c.Next() + } +} + +// (AccessHandler).AuthorizeRole +// +// Description: +// +// A role-based authorization middleware. It checks whether the authenticated +// user (based on the "role" set in AuthMiddleware) has permission to access +// the given route. The check is performed by comparing the requested URL +// path against the user’s allowed permissions. +// +// Parameters: +// - suffix: A URL prefix to trim from the request path before matching +// permissions (e.g., "/api/v1"). +// +// Behavior: +// - Fetches the user role from Gin context. +// - Uses aH.GetRoleByKey() to retrieve role records and permissions. +// - Grants access (calls c.Next()) if a matching permission is found. +// - Denies access (401 Unauthorized) if no permission matches. +// +// Usage: +// +// router.GET("/secure/:id", aH.AuthorizeRole("/api/v1")) +func (aH *AccessHandlerAPI) AuthorizeRole(suffix string) gin.HandlerFunc { + return func(c *gin.Context) { + aH.logger.Debug("AuthorizeRole", "permission path of url path") + permissionPath := strings.TrimPrefix(c.Request.URL.Path, suffix+"/") + + aH.logger.Debug("AuthorizeRole", "get set role") + role, ok := c.Get("role") + if !ok { + aH.logger.Error("AuthorizeRole", "no role set") + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"message": http.StatusInternalServerError}) + return + } + + // Fetch roles and associated permissions from the database or store + var roles []models.Role + err := aH.dbHandler.GetByKey(&roles, "role", role, false) + if err != nil { + aH.logger.Error("AuthorizeRole", err) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"message": http.StatusInternalServerError}) + return + } + + // Validate that a role was found + if len(roles) == 0 { + log.Println("not logged in") + aH.logger.Error("AuthorizeRole", "no logged in") + c.JSON(http.StatusUnauthorized, http.StatusUnauthorized) + return + } else if len(roles) > 1 { + aH.logger.Error("AuthorizeRole", "more than one record found") + c.JSON(http.StatusInternalServerError, http.StatusInternalServerError) + return + } + + // Check permissions + for _, permission := range roles[0].Permissions { + fmt.Println(100, permissionPath, permission.Name) + if permission.Name == permissionPath { + c.Next() + return + } + } + + // Access denied + aH.logger.Error("AuthorizeRole", "Forbidden") + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "Forbidden"}) + } +}