From c2059dcd6e8c0eaef3abf11e8d8a6125bd54280d Mon Sep 17 00:00:00 2001 From: Nikolai Papin Date: Tue, 24 Jun 2025 13:57:39 +0300 Subject: [PATCH] feat: middlewares for authorization and automatic request parsing; feat: roles enum --- backend/go.mod | 3 + backend/internal/controllers/auth.go | 12 ++-- backend/internal/controllers/profile.go | 8 --- backend/internal/middleware/auth.go | 22 ++++--- backend/internal/middleware/request.go | 86 +++++++++++++++++++++++++ backend/internal/routes/router.go | 10 +-- backend/internal/utils/enums/enums.go | 8 ++- 7 files changed, 123 insertions(+), 26 deletions(-) create mode 100644 backend/internal/middleware/request.go diff --git a/backend/go.mod b/backend/go.mod index a45fad6..9cde7ff 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -7,6 +7,7 @@ require ( github.com/golang-jwt/jwt/v5 v5.2.2 github.com/jackc/pgx/v5 v5.7.5 github.com/spf13/viper v1.20.1 + github.com/stretchr/testify v1.10.0 github.com/swaggo/files v1.0.1 github.com/swaggo/gin-swagger v1.6.0 github.com/swaggo/swag v1.16.4 @@ -19,6 +20,7 @@ require ( github.com/bytedance/sonic v1.13.3 // indirect github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/cloudwego/base64x v0.1.5 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/gabriel-vasile/mimetype v1.4.9 // indirect github.com/gin-contrib/sse v1.1.0 // indirect @@ -43,6 +45,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/sagikazarmark/locafero v0.9.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.14.0 // indirect diff --git a/backend/internal/controllers/auth.go b/backend/internal/controllers/auth.go index 98e01f3..6a93e33 100644 --- a/backend/internal/controllers/auth.go +++ b/backend/internal/controllers/auth.go @@ -1,25 +1,27 @@ // Copyright (c) 2025 Nikolai Papin -// +// // This file is part of Easywish -// +// // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. -// +// // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See // the GNU General Public License for more details. -// +// // You should have received a copy of the GNU General Public License // along with this program. If not, see . package controllers import ( + "easywish/internal/middleware" "easywish/internal/models" "easywish/internal/services" + "easywish/internal/utils/enums" "net/http" "github.com/gin-gonic/gin" @@ -122,7 +124,7 @@ func (a *authControllerImpl) RegistrationComplete(c *gin.Context) { } func (a *authControllerImpl) RegisterRoutes(group *gin.RouterGroup) { - group.POST("/registrationBegin", a.RegistrationBegin) + group.POST("/registrationBegin", middleware.RequestMiddleware[models.RegistrationBeginRequest](enums.GuestRole), a.RegistrationBegin) group.POST("/registrationComplete", a.RegistrationComplete) group.POST("/login", a.Login) group.POST("/refresh", a.Refresh) diff --git a/backend/internal/controllers/profile.go b/backend/internal/controllers/profile.go index c1bee1a..5a4fbd8 100644 --- a/backend/internal/controllers/profile.go +++ b/backend/internal/controllers/profile.go @@ -18,7 +18,6 @@ package controllers import ( - "easywish/internal/middleware" "net/http" "github.com/gin-gonic/gin" @@ -92,11 +91,4 @@ func (p *profileControllerImpl) UpdatePrivacySettings(c *gin.Context) { } func (p *profileControllerImpl) RegisterRoutes(group *gin.RouterGroup) { - protected := group.Group("") - protected.Use(middleware.JWTAuthMiddleware()) - { - protected.GET("/me", p.GetOwnProfile) - protected.GET("/:username", p.GetProfile) - protected.GET("/privacy", p.GetPrivacySettings) - } } diff --git a/backend/internal/middleware/auth.go b/backend/internal/middleware/auth.go index db5fd0e..f2c9a34 100644 --- a/backend/internal/middleware/auth.go +++ b/backend/internal/middleware/auth.go @@ -1,17 +1,17 @@ // Copyright (c) 2025 Nikolai Papin -// +// // This file is part of Easywish -// +// // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. -// +// // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See // the GNU General Public License for more details. -// +// // You should have received a copy of the GNU General Public License // along with this program. If not, see . @@ -19,6 +19,7 @@ package middleware import ( "easywish/config" + "easywish/internal/utils/enums" "errors" "fmt" "net/http" @@ -28,16 +29,20 @@ import ( ) type Claims struct { - Username string `json:"username"` + Username string `json:"username"` + Role enums.Role `json:"role"` jwt.RegisteredClaims } -func JWTAuthMiddleware() gin.HandlerFunc { +func AuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { cfg := config.GetConfig() authHeader := c.GetHeader("Authorization") + if authHeader == "" { - c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"}) + c.Set("username", nil) + c.Set("role", enums.GuestRole) + c.Next() return } @@ -64,7 +69,8 @@ func JWTAuthMiddleware() gin.HandlerFunc { } if claims, ok := token.Claims.(*Claims); ok && token.Valid { - c.Set("userID", claims.Username) + c.Set("username", claims.Username) + c.Set("role", claims.Role) c.Next() } else { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid claims"}) diff --git a/backend/internal/middleware/request.go b/backend/internal/middleware/request.go new file mode 100644 index 0000000..fb33d52 --- /dev/null +++ b/backend/internal/middleware/request.go @@ -0,0 +1,86 @@ +// Copyright (c) 2025 Nikolai Papin +// +// This file is part of Easywish +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See +// the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package middleware + +import ( + "easywish/internal/utils/enums" + "net/http" + + "github.com/gin-gonic/gin" +) + +type UserInfo struct { + Username string + Role enums.Role +} + +type Request[T any] struct { + User UserInfo + Body T +} + +const requestKey = "request" + +func UserInfoFromContext(c *gin.Context) (*UserInfo, bool) { + + var username any + var role any + var ok bool + + username, ok = c.Get("username") ; if !ok { + return nil, true + } + + role, ok = c.Get("role"); if !ok { + return nil, false + } + + return &UserInfo{Username: username.(string), Role: role.(enums.Role)}, true +} + +func RequestFromContext[T any](c *gin.Context) Request[T] { + return c.Value(requestKey).(Request[T]) +} + +func RequestMiddleware[T any](role enums.Role) gin.HandlerFunc { + return gin.HandlerFunc(func(c *gin.Context) { + + userInfo, ok := UserInfoFromContext(c) + + if !ok { + c.Status(http.StatusUnauthorized) + return + } + + var body T + if err := c.ShouldBindJSON(&body); err != nil { + c.JSON(http.StatusBadRequest, err) + + // TODO: implement automatic validation here + return + } + + request := Request[T]{ + User: *userInfo, + Body: body, + } + + c.Set(requestKey, request) + c.Next() + }) +} diff --git a/backend/internal/routes/router.go b/backend/internal/routes/router.go index c4c0cb4..aa60339 100644 --- a/backend/internal/routes/router.go +++ b/backend/internal/routes/router.go @@ -1,17 +1,17 @@ // Copyright (c) 2025 Nikolai Papin -// +// // This file is part of Easywish -// +// // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. -// +// // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See // the GNU General Public License for more details. -// +// // You should have received a copy of the GNU General Public License // along with this program. If not, see . @@ -19,12 +19,14 @@ package routes import ( "easywish/internal/controllers" + "easywish/internal/middleware" "github.com/gin-gonic/gin" ) func NewRouter(engine *gin.Engine, groups []RouteGroup) *gin.Engine { apiGroup := engine.Group("/api") + apiGroup.Use(middleware.AuthMiddleware()) for _, group := range groups { subgroup := apiGroup.Group(group.BasePath) subgroup.Use(group.Middleware...) diff --git a/backend/internal/utils/enums/enums.go b/backend/internal/utils/enums/enums.go index ca5ccd9..8f7cd5e 100644 --- a/backend/internal/utils/enums/enums.go +++ b/backend/internal/utils/enums/enums.go @@ -18,8 +18,14 @@ package enums type ConfirmationCodeType int32 - const ( RegistrationCodeType ConfirmationCodeType = iota PasswordResetCodeType ) + +type Role int32 +const ( + GuestRole Role = iota + UserRole + AdminRole +)