Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEAT] Authentication and Authorization #144

Merged
merged 9 commits into from
Jul 25, 2024
2 changes: 1 addition & 1 deletion internal/models/product.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
type Product struct {
ID string `gorm:"type:uuid;primaryKey" json:"product_id"`
Name string `gorm:"column:name; type:varchar(255); not null" json:"name"`
Price float64 `gorm:"column:price; type:decimal(10,2);not null" json:"price"`
Price float64 `gorm:"column:price; type:decimal(10,2);null" json:"price"`
Description string `gorm:"column:description; type:text" json:"description"`
OwnerID string `gorm:"type:uuid;" json:"owner_id"`
Category []Category `gorm:"many2many:product_categories;;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" json:"category"`
Expand Down
5 changes: 5 additions & 0 deletions internal/models/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ type LoginRequestModel struct {
Password string `json:"password" validate:"required"`
}

type ChangePasswordRequestModel struct {
OldPassword string `json:"old_password" validate:"required"`
NewPassword string `json:"new_password" validate:"required,min=7"`
}

func (u *User) AddUserToOrganisation(db *gorm.DB, user interface{}, orgs []interface{}) error {

// Add user to organisation
Expand Down
5 changes: 0 additions & 5 deletions pkg/controller/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,11 +176,6 @@ func (base *Controller) VerifyResetToken(c *gin.Context) {

}

func (base *Controller) ChangePassword(c *gin.Context) {
// to be implemented

}

func (base *Controller) RequestMagicLink(c *gin.Context) {
// to be implemented

Expand Down
44 changes: 44 additions & 0 deletions pkg/controller/auth/password.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package auth

import (
"net/http"

"github.com/gin-gonic/gin"
"github.com/hngprojects/hng_boilerplate_golang_web/internal/models"
service "github.com/hngprojects/hng_boilerplate_golang_web/services/auth"
"github.com/hngprojects/hng_boilerplate_golang_web/utility"
)

func (base *Controller) ChangePassword(c *gin.Context) {
var (
req = models.ChangePasswordRequestModel{}
)

err := c.ShouldBind(&req)
if err != nil {
rd := utility.BuildErrorResponse(http.StatusBadRequest, "error", "Failed to parse request body", err, nil)
c.JSON(http.StatusBadRequest, rd)
return
}

err = base.Validator.Struct(&req)
if err != nil {
rd := utility.BuildErrorResponse(http.StatusUnprocessableEntity, "error", "Validation failed",
utility.ValidationResponse(err, base.Validator), nil)
c.JSON(http.StatusUnprocessableEntity, rd)
return
}

respData, code, err := service.UpdateUserPassword(c, req, base.Db.Postgresql)
if err != nil {
rd := utility.BuildErrorResponse(code, "error", err.Error(), err, nil)
c.JSON(code, rd)
return
}

base.Logger.Info("password changed successfully")

rd := utility.BuildSuccessResponse(http.StatusOK, "Password updated successfully", respData)
c.JSON(http.StatusOK, rd)

}
20 changes: 20 additions & 0 deletions pkg/middleware/jwttoken.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package middleware

import (
"errors"
"fmt"
"time"

"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt"
"gorm.io/gorm"

"github.com/hngprojects/hng_boilerplate_golang_web/internal/config"
"github.com/hngprojects/hng_boilerplate_golang_web/internal/models"
Expand Down Expand Up @@ -80,3 +83,20 @@ func TokenValid(bearerToken string) (*jwt.Token, error) {
}
return token, nil
}

func GetUserClaims(c *gin.Context, db *gorm.DB, theValue string) (interface{}, error) {

claims, exists := c.Get("userClaims")
if !exists {
return nil, errors.New("user claims not found")
}

userClaims := claims.(jwt.MapClaims)
userValue, ok := userClaims[theValue]
if !ok {
return nil, errors.New("invalid value")
}

return userValue, nil

}
6 changes: 4 additions & 2 deletions pkg/router/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/go-playground/validator/v10"

"github.com/hngprojects/hng_boilerplate_golang_web/external/request"
"github.com/hngprojects/hng_boilerplate_golang_web/internal/models"
"github.com/hngprojects/hng_boilerplate_golang_web/pkg/controller/auth"
"github.com/hngprojects/hng_boilerplate_golang_web/pkg/middleware"
"github.com/hngprojects/hng_boilerplate_golang_web/pkg/repository/storage"
Expand All @@ -28,10 +29,11 @@ func Auth(r *gin.Engine, ApiVersion string, validator *validator.Validate, db *s
authUrl.POST("/magick-link/verify", auth.VerifyMagicLink)
}

authUrlSec := r.Group(fmt.Sprintf("%v/auth", ApiVersion), middleware.Authorize(db.Postgresql))
authUrlSec := r.Group(fmt.Sprintf("%v/auth", ApiVersion),
middleware.Authorize(db.Postgresql, models.RoleIdentity.SuperAdmin, models.RoleIdentity.User))
{
authUrlSec.POST("/logout", auth.LogoutUser)
authUrlSec.POST("/change-password", auth.ChangePassword)
authUrlSec.PUT("/change-password", auth.ChangePassword)
}
return r
}
4 changes: 4 additions & 0 deletions services/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@ func CreateAdmin(req models.CreateUserRequestModel, db *gorm.DB) (gin.H, int, er
"role": models.AdminRoleName,
"expires_in": tokenData.ExpiresAt.Unix(),
"access_token": tokenData.AccessToken,
"created_at": user.CreatedAt,
"updated_at": user.UpdatedAt,
}

return responseData, http.StatusCreated, nil
Expand Down Expand Up @@ -248,6 +250,8 @@ func LoginUser(req models.LoginRequestModel, db *gorm.DB) (gin.H, int, error) {
"role": userData.Role,
"expires_in": tokenData.ExpiresAt.Unix(),
"access_token": tokenData.AccessToken,
"created_at": userData.CreatedAt,
"updated_at": userData.UpdatedAt,
}

return responseData, http.StatusOK, nil
Expand Down
54 changes: 54 additions & 0 deletions services/auth/password.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package auth

import (
"errors"
"fmt"
"net/http"

"github.com/gin-gonic/gin"
"github.com/hngprojects/hng_boilerplate_golang_web/internal/models"
"github.com/hngprojects/hng_boilerplate_golang_web/pkg/middleware"
"github.com/hngprojects/hng_boilerplate_golang_web/utility"
"gorm.io/gorm"
)

func UpdateUserPassword(c *gin.Context, req models.ChangePasswordRequestModel, db *gorm.DB) (*models.User, int, error) {

user := models.User{}

userId, err := middleware.GetUserClaims(c, db, "user_id")
if err != nil {
return nil, http.StatusNotFound, err
}

userID, ok := userId.(string)
if !ok {
return nil, http.StatusBadRequest, errors.New("user_id is not of type string")
}

userDataExist, err := user.GetUserByID(db, userID)
if err != nil {
return nil, http.StatusNotFound, fmt.Errorf("unable to fetch user " + err.Error())
}

if !utility.CompareHash(req.OldPassword, userDataExist.Password) {
return nil, http.StatusBadRequest, fmt.Errorf("old password is incorrect")
}

if req.OldPassword == req.NewPassword {
return nil, http.StatusConflict, errors.New("new password cannot be the same as the old password")
}

hashedPassword, err := utility.HashPassword(req.NewPassword)
if err != nil {
return nil, http.StatusBadRequest, err
}

userDataExist.Password = hashedPassword
err = userDataExist.Update(db)
if err != nil {
return nil, http.StatusBadRequest, err
}

return &userDataExist, http.StatusOK, nil
}
16 changes: 16 additions & 0 deletions tests/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,22 @@ func AssertBool(t *testing.T, got, expected bool) {
}
}

func AssertValidationError(t *testing.T, response map[string]interface{}, field string, expectedMessage string) {
errors, ok := response["error"].(map[string]interface{})
if !ok {
t.Fatalf("expected 'error' field in response")
}

errorMessage, exists := errors[field]
if !exists {
t.Fatalf("expected validation error message for field '%s'", field)
}

if errorMessage != expectedMessage {
t.Errorf("unexpected error message for field '%s': got %v, want %v", field, errorMessage, expectedMessage)
}
}

// helper to signup a user
func SignupUser(t *testing.T, r *gin.Engine, auth auth.Controller, userSignUpData models.CreateUserRequestModel) {
var (
Expand Down
4 changes: 3 additions & 1 deletion tests/test_auth/auth_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package test_waitlist
package test_auth

import (
"bytes"
Expand Down Expand Up @@ -134,6 +134,7 @@ func TestUserSignup(t *testing.T) {
}

}

// test admin signup
func TestAdminSignup(t *testing.T) {
logger := tst.Setup()
Expand Down Expand Up @@ -352,6 +353,7 @@ func TestLogin(t *testing.T) {
}

}

// test user logout
func TestLogout(t *testing.T) {
logger := tst.Setup()
Expand Down
35 changes: 35 additions & 0 deletions tests/test_auth/base.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package test_auth

import (
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"github.com/hngprojects/hng_boilerplate_golang_web/internal/models"
"github.com/hngprojects/hng_boilerplate_golang_web/pkg/controller/auth"
"github.com/hngprojects/hng_boilerplate_golang_web/pkg/middleware"
"github.com/hngprojects/hng_boilerplate_golang_web/pkg/repository/storage"
"github.com/hngprojects/hng_boilerplate_golang_web/tests"
)

func SetupAuthTestRouter() (*gin.Engine, *auth.Controller) {
gin.SetMode(gin.TestMode)

logger := tests.Setup()
db := storage.Connection()
validator := validator.New()

authController := &auth.Controller{
Db: db,
Validator: validator,
Logger: logger,
}

r := gin.Default()
SetupAuthRoutes(r, authController)
return r, authController
}

func SetupAuthRoutes(r *gin.Engine, userController *auth.Controller) {
r.PUT("/api/v1/auth/change-password",
middleware.Authorize(userController.Db.Postgresql, models.RoleIdentity.SuperAdmin, models.RoleIdentity.User),
userController.ChangePassword)
}
Loading
Loading