Compare commits

..

5 Commits

Author SHA1 Message Date
$(pass /github/name)
0aa332c26b adapted template to new routing 2024-09-05 16:40:46 +02:00
$(pass /github/name)
600cd83a81 add: user_service GetUserByID Function 2024-09-05 16:40:23 +02:00
$(pass /github/name)
580b1523f9 chg: routing and added auth to backend endpoint 2024-09-05 16:39:47 +02:00
$(pass /github/name)
8113b02356 chg: auth handling to jwt cookies 2024-09-05 16:39:06 +02:00
$(pass /github/name)
4e5e0963c7 add: better logging in user controller and tests 2024-09-05 16:38:23 +02:00
7 changed files with 143 additions and 76 deletions

View File

@@ -27,6 +27,23 @@ type RegistrationData struct {
User models.User `json:"user"` User models.User `json:"user"`
} }
func (uc *UserController) CurrentUserHandler(c *gin.Context) {
userID, err := middlewares.GetUserIDFromContext(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Failed to authenticate user"})
c.Abort()
return
}
user, err := uc.Service.GetUserByID(userID)
if err != nil {
logger.Error.Printf("Error retrieving valid user: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error retrieving user."})
return
}
c.JSON(http.StatusOK, user)
}
func (uc *UserController) LoginUser(c *gin.Context) { func (uc *UserController) LoginUser(c *gin.Context) {
var input struct { var input struct {
Email string `json:"email"` Email string `json:"email"`
@@ -66,9 +83,19 @@ func (uc *UserController) LoginUser(c *gin.Context) {
return return
} }
c.SetCookie(
"jwt",
token,
10*60, // 10 minutes
"/",
"",
true,
true,
)
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"message": "Login successful", "message": "Login successful",
"token": token, "set-token": token,
}) })
} }
@@ -82,14 +109,14 @@ func (uc *UserController) RegisterUser(c *gin.Context) {
return return
} }
if regData.User.Membership.SubscriptionModel.Name == "" { if regData.User.Membership.SubscriptionModel.Name == "" {
logger.Error.Printf("No subscription model provided") logger.Error.Printf("No subscription model provided: %v", regData.User.Email)
c.JSON(http.StatusNotAcceptable, gin.H{"error": "No subscription model provided"}) c.JSON(http.StatusNotAcceptable, gin.H{"error": "No subscription model provided"})
return return
} }
selectedModel, err := uc.MembershipService.GetModelByName(&regData.User.Membership.SubscriptionModel.Name) selectedModel, err := uc.MembershipService.GetModelByName(&regData.User.Membership.SubscriptionModel.Name)
if err != nil { if err != nil {
logger.Error.Printf("No subscription model found: %#v", err) logger.Error.Printf("%v:No subscription model found: %#v", regData.User.Email, err)
c.JSON(http.StatusNotFound, gin.H{"error": "Not a valid subscription model"}) c.JSON(http.StatusNotFound, gin.H{"error": "Not a valid subscription model"})
return return
} }
@@ -101,7 +128,7 @@ func (uc *UserController) RegisterUser(c *gin.Context) {
// Register User // Register User
id, token, err := uc.Service.RegisterUser(&regData.User) id, token, err := uc.Service.RegisterUser(&regData.User)
if err != nil { if err != nil {
logger.Error.Printf("Couldn't register User: %v", err) logger.Error.Printf("Couldn't register User(%v): %v", regData.User.Email, err)
c.JSON(int(id), gin.H{"error": fmt.Sprintf("Couldn't register User: %v", err)}) c.JSON(int(id), gin.H{"error": fmt.Sprintf("Couldn't register User: %v", err)})
return return
} }
@@ -125,7 +152,7 @@ func (uc *UserController) RegisterUser(c *gin.Context) {
for _, consent := range consents { for _, consent := range consents {
_, err = uc.ConsentService.RegisterConsent(&consent) _, err = uc.ConsentService.RegisterConsent(&consent)
if err != nil { if err != nil {
logger.Error.Printf("Couldn't register consent: %v", err) logger.Error.Printf("%v, Couldn't register consent: %v", regData.User.Email, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Couldn't register User-consent"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Couldn't register User-consent"})
return return
} }
@@ -133,13 +160,13 @@ func (uc *UserController) RegisterUser(c *gin.Context) {
// Send notifications // Send notifications
if err := uc.EmailService.SendVerificationEmail(&regData.User, &token); err != nil { if err := uc.EmailService.SendVerificationEmail(&regData.User, &token); err != nil {
logger.Error.Printf("Failed to send email verification email to user: %v", err) logger.Error.Printf("Failed to send email verification email to user(%v): %v", regData.User.Email, err)
// Proceed without returning error since user registration is successful // Proceed without returning error since user registration is successful
} }
// Notify admin of new user registration // Notify admin of new user registration
if err := uc.EmailService.SendRegistrationNotification(&regData.User); err != nil { if err := uc.EmailService.SendRegistrationNotification(&regData.User); err != nil {
logger.Error.Printf("Failed to notify admin of new user registration: %v", err) logger.Error.Printf("Failed to notify admin of new user(%v) registration: %v", regData.User.Email, err)
// Proceed without returning error since user registration is successful // Proceed without returning error since user registration is successful
} }
c.JSON(http.StatusCreated, gin.H{ c.JSON(http.StatusCreated, gin.H{

View File

@@ -3,6 +3,7 @@ package controllers
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
@@ -40,7 +41,8 @@ func (rt *RegisterUserTest) RunHandler(c *gin.Context, router *gin.Engine) {
func (rt *RegisterUserTest) ValidateResponse(w *httptest.ResponseRecorder) error { func (rt *RegisterUserTest) ValidateResponse(w *httptest.ResponseRecorder) error {
if w.Code != rt.WantResponse { if w.Code != rt.WantResponse {
return fmt.Errorf("Didn't get the expected response code: got: %v; expected: %v", w.Code, rt.WantResponse) responseBody, _ := io.ReadAll(w.Body)
return fmt.Errorf("Register User: Didn't get the expected response code: got: %v; expected: %v. Context: %#v", w.Code, rt.WantResponse, string(responseBody))
} }
return nil return nil
} }
@@ -55,7 +57,7 @@ func TestUserController(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.Name, func(t *testing.T) { t.Run(tt.Name, func(t *testing.T) {
if err := runSingleTest(&tt); err != nil { if err := runSingleTest(&tt); err != nil {
t.Errorf("Test failed: %v", err.Error()) t.Fatalf("Test failed: %v", err.Error())
} }
}) })
} }
@@ -118,10 +120,10 @@ func testLoginUser(t *testing.T) {
if tt.wantToken { if tt.wantToken {
logger.Info.Printf("Response: %#v", response) logger.Info.Printf("Response: %#v", response)
assert.Contains(t, response, "token") assert.Contains(t, response, "set-token")
assert.NotEmpty(t, response["token"]) assert.NotEmpty(t, response["set-token"])
} else { } else {
assert.NotContains(t, response, "token") assert.NotContains(t, response, "set-token")
} }
}) })
} }
@@ -291,7 +293,7 @@ func verifyMail(verificationURL string) error {
router := gin.New() router := gin.New()
router.LoadHTMLGlob(filepath.Join(config.Templates.HTMLPath, "*")) router.LoadHTMLGlob(filepath.Join(config.Templates.HTMLPath, "*"))
router.GET("/verify", Uc.VerifyMailHandler) router.GET("/users/verify", Uc.VerifyMailHandler)
wv := httptest.NewRecorder() wv := httptest.NewRecorder()
cv, _ := gin.CreateTestContext(wv) cv, _ := gin.CreateTestContext(wv)
var err error var err error
@@ -301,7 +303,10 @@ func verifyMail(verificationURL string) error {
} }
router.ServeHTTP(wv, cv.Request) router.ServeHTTP(wv, cv.Request)
if wv.Code != 200 { if wv.Code != 200 {
return fmt.Errorf("Didn't get the expected response code: got: %v; expected: %v", wv.Code, 200)
responseBody, _ := io.ReadAll(wv.Body)
return fmt.Errorf("VerifyMail: Didn't get the expected response code: got: %v; expected: %v Context: %#v", wv.Code, 200, string(responseBody))
} }
return nil return nil
} }

View File

@@ -3,8 +3,8 @@ package middlewares
import ( import (
"GoMembership/internal/config" "GoMembership/internal/config"
"GoMembership/pkg/logger" "GoMembership/pkg/logger"
"fmt"
"net/http" "net/http"
"strings"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -20,9 +20,10 @@ var (
func GenerateToken(userID int64) (string, error) { func GenerateToken(userID int64) (string, error) {
token := jwt.NewWithClaims(jwtSigningMethod, jwt.MapClaims{ token := jwt.NewWithClaims(jwtSigningMethod, jwt.MapClaims{
"user_id": userID, "user_id": userID,
"exp": time.Now().Add(time.Minute * 15).Unix(), // Token expires in 15 Minutes "exp": time.Now().Add(time.Minute * 10).Unix(), // Token expires in 10 Minutes
}) })
logger.Error.Printf("token generated: %#v", token)
return token.SignedString(jwtKey) return token.SignedString(jwtKey)
} }
@@ -37,45 +38,71 @@ func verifyToken(tokenString string) (*jwt.Token, error) {
return token, nil return token, nil
} }
func GetUserIDFromContext(c *gin.Context) (int64, error) {
tokenString, err := c.Cookie("jwt")
if err != nil {
logger.Error.Printf("Error getting cookie: %v\n", err)
return 0, err
}
if tokenString == "" {
logger.Error.Printf("Token is empty: %v\n", err)
return 0, fmt.Errorf("Authorization token is required")
}
token, err := verifyToken(tokenString)
if err != nil || !token.Valid {
logger.Error.Printf("Token is invalid: %v\n", err)
return 0, fmt.Errorf("Token not valid!")
}
claims, ok := token.Claims.(jwt.MapClaims)
logger.Error.Printf("claims userid: %v", claims["user_id"].(float64))
if !ok {
logger.Error.Printf("Invalid Token claims")
return 0, fmt.Errorf("Invalid token claims")
}
userID, ok := claims["user_id"].(float64)
if !ok {
logger.Error.Printf("Invalid user ID: %v", userID)
return 0, fmt.Errorf("Invalid user ID")
}
return int64(userID), nil
}
func AuthMiddleware() gin.HandlerFunc { func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is required"})
c.Abort()
return
}
bearerToken := strings.Split(authHeader, " ") userID, err := GetUserIDFromContext(c)
if len(bearerToken) != 2 {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token format"})
c.Abort()
return
}
tokenString := bearerToken[1]
token, err := verifyToken(tokenString)
if err != nil { if err != nil {
if err == jwt.ErrTokenSignatureInvalid { c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
logger.Error.Printf("JWT NULL ATTACK: %#v", err)
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token signing method"})
} else {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
}
c.Abort() c.Abort()
return return
} }
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { // Generate a new token
userID := claims["user_id"].(string) newToken, err := GenerateToken(int64(userID))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to refresh token"})
c.Abort()
return
}
c.SetCookie(
"jwt",
newToken,
10*60, // 10 minutes
"/",
"",
true,
true,
)
c.Set("user_id", userID) c.Set("user_id", userID)
c.Next() c.Next()
} else {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token claims"})
c.Abort()
return
}
} }
} }

View File

@@ -24,11 +24,11 @@ func TestAuthMiddleware(t *testing.T) {
{ {
name: "Valid Token", name: "Valid Token",
setupAuth: func(r *http.Request) { setupAuth: func(r *http.Request) {
token, _ := GenerateToken("user123") token, _ := GenerateToken(123)
r.Header.Set("Authorization", "Bearer "+token) r.AddCookie(&http.Cookie{Name: "jwt", Value: token})
}, },
expectedStatus: http.StatusOK, expectedStatus: http.StatusOK,
expectedUserID: 12, expectedUserID: 123,
}, },
{ {
name: "Missing Auth Header", name: "Missing Auth Header",
@@ -52,7 +52,7 @@ func TestAuthMiddleware(t *testing.T) {
"exp": time.Now().Add(-time.Hour).Unix(), // Expired 1 hour ago "exp": time.Now().Add(-time.Hour).Unix(), // Expired 1 hour ago
}) })
tokenString, _ := token.SignedString(jwtKey) tokenString, _ := token.SignedString(jwtKey)
r.Header.Set("Authorization", "Bearer "+tokenString) r.AddCookie(&http.Cookie{Name: "jwt", Value: tokenString})
}, },
expectedStatus: http.StatusUnauthorized, expectedStatus: http.StatusUnauthorized,
expectedUserID: 0, expectedUserID: 0,
@@ -65,17 +65,11 @@ func TestAuthMiddleware(t *testing.T) {
"exp": time.Now().Add(time.Hour).Unix(), "exp": time.Now().Add(time.Hour).Unix(),
}) })
tokenString, _ := token.SignedString([]byte("wrong_secret")) tokenString, _ := token.SignedString([]byte("wrong_secret"))
r.Header.Set("Authorization", "Bearer "+tokenString) r.AddCookie(&http.Cookie{Name: "jwt", Value: tokenString})
}, },
expectedStatus: http.StatusUnauthorized, expectedStatus: http.StatusUnauthorized,
expectedUserID: 0, expectedUserID: 0,
}, },
{
name: "Missing Auth Header",
setupAuth: func(r *http.Request) {},
expectedStatus: http.StatusUnauthorized,
expectedUserID: 0,
},
} }
for _, tt := range tests { for _, tt := range tests {
@@ -100,11 +94,20 @@ func TestAuthMiddleware(t *testing.T) {
assert.Equal(t, tt.expectedStatus, w.Code) assert.Equal(t, tt.expectedStatus, w.Code)
var response map[string]string if tt.expectedStatus == http.StatusOK {
var response map[string]int64
err := json.Unmarshal(w.Body.Bytes(), &response) err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, tt.expectedUserID, response["user_id"]) assert.Equal(t, tt.expectedUserID, response["user_id"])
// Check if a new cookie was set
cookies := w.Result().Cookies()
assert.GreaterOrEqual(t, len(cookies), 1)
assert.Equal(t, "jwt", cookies[0].Name)
assert.NotEmpty(t, cookies[0].Value)
} else {
assert.Equal(t, 0, len(w.Result().Cookies()))
}
}) })
} }
} }

View File

@@ -8,9 +8,9 @@ import (
) )
func RegisterRoutes(router *gin.Engine, userController *controllers.UserController, membershipcontroller *controllers.MembershipController, contactController *controllers.ContactController) { func RegisterRoutes(router *gin.Engine, userController *controllers.UserController, membershipcontroller *controllers.MembershipController, contactController *controllers.ContactController) {
router.GET("/verify", userController.VerifyMailHandler) router.GET("/users/verify", userController.VerifyMailHandler)
router.POST("/h/register", userController.RegisterUser) router.POST("/users/register", userController.RegisterUser)
router.POST("/h/contact", contactController.RelayContactRequest) router.POST("/users/contact", contactController.RelayContactRequest)
router.POST("/csp-report", middlewares.CSPReportHandling) router.POST("/csp-report", middlewares.CSPReportHandling)
@@ -21,7 +21,13 @@ func RegisterRoutes(router *gin.Engine, userController *controllers.UserControll
//create api key required router //create api key required router
apiRouter := router.Group("/api") apiRouter := router.Group("/api")
{ {
router.POST("/subscription", membershipcontroller.RegisterSubscription) router.POST("/v1/subscription", membershipcontroller.RegisterSubscription)
} }
apiRouter.Use(middlewares.APIKeyMiddleware()) apiRouter.Use(middlewares.APIKeyMiddleware())
authRouter := router.Group("/users/backend")
{
router.POST("/currentUser", userController.CurrentUserHandler)
}
authRouter.Use(middlewares.AuthMiddleware())
} }

View File

@@ -1,7 +1,6 @@
package services package services
import ( import (
"fmt"
"net/http" "net/http"
"strings" "strings"
@@ -20,6 +19,7 @@ import (
type UserServiceInterface interface { type UserServiceInterface interface {
RegisterUser(user *models.User) (int64, string, error) RegisterUser(user *models.User) (int64, string, error)
GetUserByEmail(email string) (*models.User, error) GetUserByEmail(email string) (*models.User, error)
GetUserByID(id int64) (*models.User, error)
GetUsers(where map[string]interface{}) (*[]models.User, error) GetUsers(where map[string]interface{}) (*[]models.User, error)
VerifyUser(token *string) (*models.User, error) VerifyUser(token *string) (*models.User, error)
} }
@@ -69,14 +69,7 @@ func (service *UserService) Update(user *models.User) (int64, string, error) {
return http.StatusNotAcceptable, "", err return http.StatusNotAcceptable, "", err
} }
if user.Password == "" && user.RoleID != constants.Roles.Member { setPassword(user.Password, user)
return http.StatusNotAcceptable, "", fmt.Errorf("No password provided")
}
hash, err := utils.HashPassword(user.Password)
if err != nil {
return http.StatusInternalServerError, "", err
}
user.Password = hash
user.Status = constants.UnverifiedStatus user.Status = constants.UnverifiedStatus
user.CreatedAt = time.Now() user.CreatedAt = time.Now()
@@ -106,6 +99,12 @@ func (service *UserService) Update(user *models.User) (int64, string, error) {
return id, token, nil return id, token, nil
} }
func (service *UserService) GetUserByID(id int64) (*models.User, error) {
return service.Repo.GetUserByID(id)
}
func (service *UserService) GetUserByEmail(email string) (*models.User, error) { func (service *UserService) GetUserByEmail(email string) (*models.User, error) {
return service.Repo.GetUserByEmail(email) return service.Repo.GetUserByEmail(email)
} }

View File

@@ -8,7 +8,7 @@ noch Ihre Emailadresse indem Sie hier klicken:
E-Mail Adresse bestätigen E-Mail Adresse bestätigen
{{.BASEURL}}/verify?token={{.Token}} {{.BASEURL}}/users/verify?token={{.Token}}
Nachdem wir Ihre E-Mail Adresse bestätigen konnten, schicken wir Nachdem wir Ihre E-Mail Adresse bestätigen konnten, schicken wir
Ihnen alle weiteren Informationen zu. Wir freuen uns auf die Ihnen alle weiteren Informationen zu. Wir freuen uns auf die
@@ -94,7 +94,7 @@ Der Vorstand
</div> </div>
<div style="text-align: center; padding: 16px 24px 16px 24px"> <div style="text-align: center; padding: 16px 24px 16px 24px">
<a <a
href="{{.BASEURL}}/verify?token={{.Token}}" href="{{.BASEURL}}/users/verify?token={{.Token}}"
style=" style="
color: #ffffff; color: #ffffff;
font-size: 26px; font-size: 26px;
@@ -146,7 +146,7 @@ Der Vorstand
padding: 4px 24px 16px 24px; padding: 4px 24px 16px 24px;
" "
> >
{{.BASEURL}}/verify?token={{.Token}} {{.BASEURL}}/users/verify?token={{.Token}}
</div> </div>
<div style="font-weight: normal; padding: 16px 24px 16px 24px"> <div style="font-weight: normal; padding: 16px 24px 16px 24px">
Nachdem wir Ihre E-Mail Adresse bestätigen konnten, schicken wir Nachdem wir Ihre E-Mail Adresse bestätigen konnten, schicken wir