diff --git a/internal/controllers/membershipController_test.go b/internal/controllers/membershipController_test.go index b4726d7..58d2b54 100644 --- a/internal/controllers/membershipController_test.go +++ b/internal/controllers/membershipController_test.go @@ -6,7 +6,6 @@ import ( "net/http/httptest" "testing" - "GoMembership/internal/config" "GoMembership/internal/models" "github.com/gin-gonic/gin" @@ -64,7 +63,6 @@ func validateSubscription(assert bool, wantDBData map[string]interface{}) error func getBaseSubscription() MembershipData { return MembershipData{ - APIKey: config.Auth.APIKEY, Model: models.SubscriptionModel{ Name: "Just a Subscription", Details: "A subscription detail", @@ -80,28 +78,6 @@ func customizeSubscription(customize func(MembershipData) MembershipData) Member func getSubscriptionData() []RegisterSubscriptionTest { return []RegisterSubscriptionTest{ - { - Name: "No API Key should fail", - WantResponse: http.StatusUnauthorized, - WantDBData: map[string]interface{}{"name": "Just a Subscription"}, - Assert: false, - Input: GenerateInputJSON( - customizeSubscription(func(subscription MembershipData) MembershipData { - subscription.APIKey = "" - return subscription - })), - }, - { - Name: "Wrong API Key should fail", - WantResponse: http.StatusUnauthorized, - WantDBData: map[string]interface{}{"name": "Just a Subscription"}, - Assert: false, - Input: GenerateInputJSON( - customizeSubscription(func(subscription MembershipData) MembershipData { - subscription.APIKey = "alskfdlkjsfjk23-dF" - return subscription - })), - }, { Name: "No Details should fail", WantResponse: http.StatusNotAcceptable, diff --git a/internal/controllers/user_controller.go b/internal/controllers/user_controller.go index 5c6e816..cad8285 100644 --- a/internal/controllers/user_controller.go +++ b/internal/controllers/user_controller.go @@ -3,6 +3,8 @@ package controllers import ( "fmt" + "GoMembership/internal/constants" + "GoMembership/internal/middlewares" "GoMembership/internal/models" "GoMembership/internal/services" @@ -25,6 +27,51 @@ type RegistrationData struct { User models.User `json:"user"` } +func (uc *UserController) LoginUser(c *gin.Context) { + var input struct { + Email string `json:"email"` + Password string `json:"password"` + } + + if err := c.ShouldBindJSON(&input); err != nil { + logger.Error.Printf("Couldn't decode input: %v", err.Error()) + c.JSON(http.StatusBadRequest, gin.H{"error": "Couldn't decode request data"}) + return + } + + user, err := uc.Service.GetUserByEmail(input.Email) + if err != nil { + logger.Error.Printf("Error during user(%v) retrieval: %v\n", input.Email, err) + c.JSON(http.StatusNotFound, gin.H{"error": "Couldn't find user"}) + return + } + + ok, err := user.PasswordMatches(input.Password) + if err != nil { + + logger.Error.Printf("Error during Password comparison: %v", err.Error()) + c.JSON(http.StatusInternalServerError, gin.H{"error": "couldn't calculate match"}) + return + } + if !ok { + + logger.Error.Printf("Wrong Password: %v %v", user.FirstName, user.LastName) + c.JSON(http.StatusNotAcceptable, gin.H{"error": "Wrong Password"}) + return + } + + token, err := middlewares.GenerateToken(user.ID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate JWT token"}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "message": "Login successful", + "token": token, + }) +} + func (uc *UserController) RegisterUser(c *gin.Context) { var regData RegistrationData @@ -48,6 +95,9 @@ func (uc *UserController) RegisterUser(c *gin.Context) { } regData.User.Membership.SubscriptionModel = *selectedModel // logger.Info.Printf("REGISTERING user: %#v", regData.User) + + regData.User.RoleID = constants.Roles.Member + // Register User id, token, err := uc.Service.RegisterUser(®Data.User) if err != nil { @@ -93,8 +143,8 @@ func (uc *UserController) RegisterUser(c *gin.Context) { // Proceed without returning error since user registration is successful } c.JSON(http.StatusCreated, gin.H{ - "status": "success", - "id": regData.User.ID, + "message": "Registration successuful", + "id": regData.User.ID, }) } diff --git a/internal/controllers/user_controller_test.go b/internal/controllers/user_controller_test.go index 2acdf11..14c99a1 100644 --- a/internal/controllers/user_controller_test.go +++ b/internal/controllers/user_controller_test.go @@ -1,6 +1,7 @@ package controllers import ( + "encoding/json" "fmt" "net/http" "net/http/httptest" @@ -12,6 +13,7 @@ import ( "time" "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" "GoMembership/internal/config" "GoMembership/internal/constants" @@ -57,8 +59,74 @@ func TestUserController(t *testing.T) { } }) } + testLoginUser(t) } +func testLoginUser(t *testing.T) { + // This test should run after the user registration test + t.Run("LoginUser", func(t *testing.T) { + // Test cases + tests := []struct { + name string + input string + wantStatusCode int + wantToken bool + }{ + { + name: "Valid login", + input: `{ + "email": "john.doe@example.com", + "password": "password123" + }`, + wantStatusCode: http.StatusOK, + wantToken: true, + }, + { + name: "Invalid email", + input: `{ + "email": "nonexistent@example.com", + "password": "password123" + }`, + wantStatusCode: http.StatusNotFound, + wantToken: false, + }, + { + name: "Invalid password", + input: `{ + "email": "john.doe@example.com", + "password": "wrongpassword" + }`, + wantStatusCode: http.StatusNotAcceptable, + wantToken: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Setup + c, w, _ := GetMockedJSONContext([]byte(tt.input), "/login") + + // Execute + Uc.LoginUser(c) + + // Assert + assert.Equal(t, tt.wantStatusCode, w.Code) + + var response map[string]interface{} + err := json.Unmarshal(w.Body.Bytes(), &response) + assert.NoError(t, err) + + if tt.wantToken { + logger.Info.Printf("Response: %#v", response) + assert.Contains(t, response, "token") + assert.NotEmpty(t, response["token"]) + } else { + assert.NotContains(t, response, "token") + } + }) + } + }) +} func validateUser(assert bool, wantDBData map[string]interface{}) error { users, err := Uc.Service.GetUsers(wantDBData) if err != nil { @@ -223,7 +291,7 @@ func verifyMail(verificationURL string) error { router := gin.New() router.LoadHTMLGlob(filepath.Join(config.Templates.HTMLPath, "*")) - router.GET("/backend/verify", Uc.VerifyMailHandler) + router.GET("/verify", Uc.VerifyMailHandler) wv := httptest.NewRecorder() cv, _ := gin.CreateTestContext(wv) var err error diff --git a/internal/middlewares/auth.go b/internal/middlewares/auth.go index 30e6233..57a6311 100644 --- a/internal/middlewares/auth.go +++ b/internal/middlewares/auth.go @@ -17,7 +17,7 @@ var ( jwtParser = jwt.NewParser(jwt.WithValidMethods([]string{jwtSigningMethod.Alg()})) ) -func GenerateToken(userID string) (string, error) { +func GenerateToken(userID int64) (string, error) { token := jwt.NewWithClaims(jwtSigningMethod, jwt.MapClaims{ "user_id": userID, "exp": time.Now().Add(time.Minute * 15).Unix(), // Token expires in 15 Minutes diff --git a/internal/middlewares/auth_test.go b/internal/middlewares/auth_test.go index 6d5cb73..531354c 100644 --- a/internal/middlewares/auth_test.go +++ b/internal/middlewares/auth_test.go @@ -19,7 +19,7 @@ func TestAuthMiddleware(t *testing.T) { name string setupAuth func(r *http.Request) expectedStatus int - expectedUserID string + expectedUserID int64 }{ { name: "Valid Token", @@ -28,13 +28,13 @@ func TestAuthMiddleware(t *testing.T) { r.Header.Set("Authorization", "Bearer "+token) }, expectedStatus: http.StatusOK, - expectedUserID: "user123", + expectedUserID: 12, }, { name: "Missing Auth Header", setupAuth: func(r *http.Request) {}, expectedStatus: http.StatusUnauthorized, - expectedUserID: "", + expectedUserID: 0, }, { name: "Invalid Token Format", @@ -42,7 +42,7 @@ func TestAuthMiddleware(t *testing.T) { r.Header.Set("Authorization", "InvalidFormat") }, expectedStatus: http.StatusUnauthorized, - expectedUserID: "", + expectedUserID: 0, }, { name: "Expired Token", @@ -55,7 +55,7 @@ func TestAuthMiddleware(t *testing.T) { r.Header.Set("Authorization", "Bearer "+tokenString) }, expectedStatus: http.StatusUnauthorized, - expectedUserID: "", + expectedUserID: 0, }, { name: "Invalid Signature", @@ -68,13 +68,13 @@ func TestAuthMiddleware(t *testing.T) { r.Header.Set("Authorization", "Bearer "+tokenString) }, expectedStatus: http.StatusUnauthorized, - expectedUserID: "", + expectedUserID: 0, }, { name: "Missing Auth Header", setupAuth: func(r *http.Request) {}, expectedStatus: http.StatusUnauthorized, - expectedUserID: "", + expectedUserID: 0, }, } @@ -88,7 +88,7 @@ func TestAuthMiddleware(t *testing.T) { if exists { c.JSON(http.StatusOK, gin.H{"user_id": userID}) } else { - c.JSON(http.StatusUnauthorized, gin.H{"user_id": ""}) + c.JSON(http.StatusUnauthorized, gin.H{"user_id": 0}) } }) diff --git a/internal/models/user.go b/internal/models/user.go index ae85b34..911c398 100644 --- a/internal/models/user.go +++ b/internal/models/user.go @@ -2,20 +2,21 @@ package models import ( "GoMembership/internal/constants" - "gorm.io/gorm" "time" + + "github.com/alexedwards/argon2id" + "gorm.io/gorm" ) type User struct { UpdatedAt time.Time DateOfBirth time.Time `gorm:"not null" json:"date_of_birth" validate:"required,age"` CreatedAt time.Time - Salt *string `json:"-"` Company string `json:"company" validate:"omitempty,omitnil"` Phone string `json:"phone" validate:"omitempty,omitnil"` Notes *string `json:"notes"` FirstName string `gorm:"not null" json:"first_name" validate:"required"` - Password string `json:"password"` + Password string `json:"password" required_unless=RoleID 0` Email string `gorm:"unique;not null" json:"email" validate:"required,email"` LastName string `gorm:"not null" json:"last_name" validate:"required"` ProfilePicture string `json:"profile_picture" validate:"omitempty,omitnil,image"` @@ -40,3 +41,12 @@ func (u *User) BeforeCreate(tx *gorm.DB) (err error) { } return } + +func (u *User) PasswordMatches(plaintextPassword string) (bool, error) { + match, err := argon2id.ComparePasswordAndHash(plaintextPassword, u.Password) + if err != nil { + return false, err + } + + return match, nil +} diff --git a/internal/repositories/user_repository.go b/internal/repositories/user_repository.go index e523934..f58583c 100644 --- a/internal/repositories/user_repository.go +++ b/internal/repositories/user_repository.go @@ -18,8 +18,8 @@ type UserRepositoryInterface interface { CreateUser(user *models.User) (int64, error) UpdateUser(userID int64, user *models.User) error GetUsers(where map[string]interface{}) (*[]models.User, error) - FindUserByID(id int64) (*models.User, error) - FindUserByEmail(email string) (*models.User, error) + GetUserByID(id int64) (*models.User, error) + GetUserByEmail(email string) (*models.User, error) SetVerificationToken(user *models.User, token *string) (int64, error) IsVerified(userID *int64) (bool, error) VerifyUserOfToken(token *string) (*models.User, error) @@ -70,7 +70,7 @@ func (ur *UserRepository) GetUsers(where map[string]interface{}) (*[]models.User return &users, nil } -func (ur *UserRepository) FindUserByID(id int64) (*models.User, error) { +func (ur *UserRepository) GetUserByID(id int64) (*models.User, error) { var user models.User result := database.DB. Preload("Consents"). @@ -88,7 +88,7 @@ func (ur *UserRepository) FindUserByID(id int64) (*models.User, error) { return &user, nil } -func (ur *UserRepository) FindUserByEmail(email string) (*models.User, error) { +func (ur *UserRepository) GetUserByEmail(email string) (*models.User, error) { var user models.User result := database.DB.Where("email = ?", email).First(&user) if result.Error != nil { @@ -127,7 +127,7 @@ func (ur *UserRepository) VerifyUserOfToken(token *string) (*models.User, error) if err != nil { return nil, err } - user, err := ur.FindUserByID(emailVerification.UserID) + user, err := ur.GetUserByID(emailVerification.UserID) if err != nil { return nil, err } diff --git a/internal/services/user_service.go b/internal/services/user_service.go index 9a534b3..ab048e2 100644 --- a/internal/services/user_service.go +++ b/internal/services/user_service.go @@ -1,25 +1,26 @@ package services import ( + "fmt" "net/http" "strings" - "github.com/go-playground/validator/v10" - "GoMembership/internal/constants" "GoMembership/internal/models" "GoMembership/internal/repositories" "GoMembership/internal/utils" "GoMembership/pkg/logger" + "github.com/alexedwards/argon2id" + "github.com/go-playground/validator/v10" + "time" ) type UserServiceInterface interface { RegisterUser(user *models.User) (int64, string, error) - FindUserByEmail(email string) (*models.User, error) + GetUserByEmail(email string) (*models.User, error) GetUsers(where map[string]interface{}) (*[]models.User, error) - // AuthenticateUser(email, password string) (*models.User, error)A VerifyUser(token *string) (*models.User, error) } @@ -28,16 +29,12 @@ type UserService struct { } func (service *UserService) RegisterUser(user *models.User) (int64, string, error) { - /* salt := make([]byte, 16) - if _, err := rand.Read(salt); err != nil { - return -1, err - } - user.Salt = base64.StdEncoding.EncodeToString(salt) - */ if err := validateRegistrationData(user); err != nil { return http.StatusNotAcceptable, "", err } + setPassword(user.Password, user) + user.Status = constants.UnverifiedStatus user.CreatedAt = time.Now() user.UpdatedAt = time.Now() @@ -67,8 +64,50 @@ func (service *UserService) RegisterUser(user *models.User) (int64, string, erro return id, token, nil } -func (service *UserService) FindUserByEmail(email string) (*models.User, error) { - return service.Repo.FindUserByEmail(email) +func (service *UserService) Update(user *models.User) (int64, string, error) { + if err := validateRegistrationData(user); err != nil { + return http.StatusNotAcceptable, "", err + } + + if user.Password == "" && user.RoleID != constants.Roles.Member { + 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.CreatedAt = time.Now() + user.UpdatedAt = time.Now() + + id, err := service.Repo.CreateUser(user) + + if err != nil && strings.Contains(err.Error(), "UNIQUE constraint failed") { + return http.StatusConflict, "", err + } else if err != nil { + return http.StatusInternalServerError, "", err + } + + user.ID = id + + token, err := utils.GenerateVerificationToken() + if err != nil { + return http.StatusInternalServerError, "", err + } + + logger.Info.Printf("TOKEN: %v", token) + + _, err = service.Repo.SetVerificationToken(user, &token) + if err != nil { + return http.StatusInternalServerError, "", err + } + + return id, token, nil +} +func (service *UserService) GetUserByEmail(email string) (*models.User, error) { + return service.Repo.GetUserByEmail(email) } func (service *UserService) GetUsers(where map[string]interface{}) (*[]models.User, error) { @@ -90,39 +129,15 @@ func validateRegistrationData(user *models.User) error { validate.RegisterValidation("iban", utils.IBANValidator) validate.RegisterValidation("subscriptionModel", utils.SubscriptionModelValidator) validate.RegisterValidation("membershipField", utils.ValidateRequiredMembershipField) + return validate.Struct(user) } -/* func HashPassword(password string, salt string) (string, error) { - saltedPassword := password + salt - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(saltedPassword), bcrypt.DefaultCost) +func setPassword(plaintextPassword string, u *models.User) error { + hash, err := argon2id.CreateHash(plaintextPassword, argon2id.DefaultParams) if err != nil { - - return "", err + return err } - return base64.StdEncoding.EncodeToString(hashedPassword), nil -} */ - -/* func (s *UserService) AuthenticateUser(email, password string) (*models.User, error) { - user, err := s.repo.FindUserByEmail(email) - if err != nil { - return nil, errors.ErrUserNotFound - } - - if !verifyPassword(password, user.Password, user.Salt) { - return nil, errors.ErrInvalidCredentials - } - - return user, nil + u.Password = hash + return nil } -*/ -/* func verifyPassword(password string, storedPassword string, salt string) bool { - - saltedPassword := password + salt - decodedStoredPassword, err := base64.StdEncoding.DecodeString(storedPassword) - if err != nil { - return false - } - err = bcrypt.CompareHashAndPassword([]byte(decodedStoredPassword), []byte(saltedPassword)) - return err == nil -} */