diff --git a/domain/entity/all_entities.go b/domain/entity/all_entities.go index 737a775..9ff3910 100644 --- a/domain/entity/all_entities.go +++ b/domain/entity/all_entities.go @@ -21,8 +21,7 @@ var allTables = []any{ &UserHasRoles{}, &Role{}, - // ... - // Add more tables/structs + // add more tables/structs } func AllTables() []any { diff --git a/domain/model/user.go b/domain/model/user.go index 8710966..45d0a1f 100644 --- a/domain/model/user.go +++ b/domain/model/user.go @@ -1,6 +1,10 @@ package model -import "github.com/Lukmanern/gost/domain/entity" +import ( + "time" + + "github.com/Lukmanern/gost/domain/entity" +) type UserRegister struct { Name string `validate:"required,min=2,max=60" json:"name"` @@ -39,19 +43,22 @@ type UserPasswordUpdate struct { } type UserProfile struct { - Email string - Name string - Roles []string + Email string + Name string + ActivatedAt *time.Time + Roles []string } type UserResponse struct { - ID int - Name string + ID int + Name string + ActivatedAt *time.Time } type UserResponseDetail struct { - ID int - Email string - Name string - Roles []entity.Role + ID int + Email string + Name string + ActivatedAt *time.Time + Roles []entity.Role } diff --git a/internal/constants/constants.go b/internal/constants/constants.go deleted file mode 100644 index 94ab46a..0000000 --- a/internal/constants/constants.go +++ /dev/null @@ -1,19 +0,0 @@ -// Constants used for error messages and testing assertions. - -package constants - -const ( - Unauthorized = "should unauthorized" - RedisNil = "redis nil value" - NotFound = "data not found" - ServerErr = "internal server error: " - InvalidID = "invalid id" - InvalidBody = "invalid json body: " - - ShouldErr = "should error" - ShouldNotErr = "should not error" - ShouldNil = "should nil" - ShouldNotNil = "should not nil" - ShouldEqual = "should equal" - ShouldNotEqual = "should not equal" -) diff --git a/internal/consts/consts.go b/internal/consts/consts.go new file mode 100644 index 0000000..9ad102a --- /dev/null +++ b/internal/consts/consts.go @@ -0,0 +1,31 @@ +package consts + +const ( + SuccessCreated = "data successfully created" + SuccessLoaded = "data successfully loaded" + Unauthorized = "unauthorized" + BadRequest = "bad request, please check your request and try again" + NotFound = "data not found" +) + +const ( + InvalidJSONBody = "invalid JSON body" + InvalidUserID = "invalid user ID" + InvalidID = "invalid ID" + + RedisNil = "redis nil value" + + ErrGetIDFromJWT = "error while getting user ID from JWT-Claims" + ErrHashing = "error while hashing password, please try again" +) + +const ( + ShouldErr = "should error" + ShouldNotErr = "should not error" + ShouldNil = "should nil" + ShouldNotNil = "should not nil" + ShouldEqual = "should equal" + ShouldNotEqual = "should not equal" + + LoginShouldSuccess = "login should success" +) diff --git a/internal/helper/helper_test.go b/internal/helper/helper_test.go index 90a137d..ea3b019 100644 --- a/internal/helper/helper_test.go +++ b/internal/helper/helper_test.go @@ -4,7 +4,7 @@ import ( "net" "testing" - "github.com/Lukmanern/gost/internal/constants" + "github.com/Lukmanern/gost/internal/consts" "github.com/stretchr/testify/assert" ) @@ -27,27 +27,27 @@ func TestRandomIPAddress(t *testing.T) { for i := 0; i < 20; i++ { ipRand := RandomIPAddress() ip := net.ParseIP(ipRand) - assert.NotNil(t, ip, constants.ShouldNotNil) + assert.NotNil(t, ip, consts.ShouldNotNil) } } func TestValidateEmails(t *testing.T) { err1 := ValidateEmails("f", "a") - assert.Error(t, err1, constants.ShouldErr) + assert.Error(t, err1, consts.ShouldErr) err2 := ValidateEmails("validemail098@gmail.com") assert.NoError(t, err2, "should not error") err3 := ValidateEmails("validemail0911@gmail.com", "invalidemail0987@.gmail.com") - assert.Error(t, err3, constants.ShouldErr) + assert.Error(t, err3, consts.ShouldErr) err4 := ValidateEmails("validemail0987@gmail.com", "valid_email0987@gmail.com", "invalidemail0987@gmail.com.") - assert.Error(t, err4, constants.ShouldErr) + assert.Error(t, err4, consts.ShouldErr) } func TestNewFiberCtx(t *testing.T) { c := NewFiberCtx() - assert.NotNil(t, c, constants.ShouldNotNil) + assert.NotNil(t, c, consts.ShouldNotNil) } func TestToTitle(t *testing.T) { diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go index 323d52a..c57812c 100644 --- a/internal/middleware/middleware.go +++ b/internal/middleware/middleware.go @@ -2,7 +2,6 @@ package middleware import ( "crypto/rsa" - "errors" "fmt" "log" "strings" @@ -28,11 +27,11 @@ type JWTHandler struct { // Claims struct will be generated as token,contains // user data like ID, email, role and permissions. +// You can add new field if you want. type Claims struct { - ID int `json:"id"` - Email string `json:"email"` - Role string `json:"role"` - Permissions map[int]int `json:"permissions"` + ID int `json:"id"` + Email string `json:"email"` + Role string `json:"role"` jwt.RegisteredClaims } @@ -67,16 +66,12 @@ func NewJWTHandler() *JWTHandler { } // GenerateJWT func generate new token with expire time for user -func (j *JWTHandler) GenerateJWT(id int, email, role string, permissions map[int]int, expired time.Time) (t string, err error) { - if email == "" || role == "" || len(permissions) < 1 { - return "", errors.New("email/ role/ permission too short or void") - } +func (j *JWTHandler) GenerateJWT(id int, email, role string, expired time.Time) (t string, err error) { // Create Claims claims := Claims{ - ID: id, - Email: email, - Role: role, - Permissions: permissions, + ID: id, + Email: email, + Role: role, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: &jwt.NumericDate{Time: expired}, NotBefore: &jwt.NumericDate{Time: time.Now()}, @@ -211,25 +206,6 @@ func CheckHasPermission(requirePermID int, userPermissions map[int]int) bool { return true } -// HasPermission func extracts and checks for claims from fiber Ctx -func (j JWTHandler) HasPermission(c *fiber.Ctx, endpointPermID int) error { - claims, ok := c.Locals("claims").(*Claims) - if !ok { - return response.Unauthorized(c) - } - userPermissions := claims.Permissions - endpointBits := BuildBitGroups(endpointPermID) - // it seems O(n), but it's actually O(1) - // because length of $endpointBits is 1 - for key, requiredBits := range endpointBits { - userBits, ok := userPermissions[key] - if !ok || requiredBits&userBits == 0 { - return response.Unauthorized(c) - } - } - return c.Next() -} - // HasRole func check claims-role equal or not with require role func (j JWTHandler) HasRole(c *fiber.Ctx, role string) error { claims, ok := c.Locals("claims").(*Claims) @@ -239,14 +215,6 @@ func (j JWTHandler) HasRole(c *fiber.Ctx, role string) error { return c.Next() } -// CheckHasPermission func is handler/middleware that -// called before the controller for checks the fiber ctx -func (j JWTHandler) CheckHasPermission(endpointPermID int) func(c *fiber.Ctx) error { - return func(c *fiber.Ctx) error { - return j.HasPermission(c, endpointPermID) - } -} - // CheckHasRole func is handler/middleware that // called before the controller for checks the fiber ctx func (j JWTHandler) CheckHasRole(role string) func(c *fiber.Ctx) error { diff --git a/internal/middleware/middleware_test.go b/internal/middleware/middleware_test.go index 0e51b7d..e73e0aa 100644 --- a/internal/middleware/middleware_test.go +++ b/internal/middleware/middleware_test.go @@ -7,7 +7,7 @@ import ( "testing" "time" - "github.com/Lukmanern/gost/internal/constants" + "github.com/Lukmanern/gost/internal/consts" "github.com/Lukmanern/gost/internal/env" "github.com/Lukmanern/gost/internal/helper" "github.com/gofiber/fiber/v2" @@ -63,7 +63,7 @@ func TestNewJWTHandler(t *testing.T) { func TestGenerateClaims(t *testing.T) { jwtHandler := NewJWTHandler() - token, err := jwtHandler.GenerateJWT(1, params.Email, params.Role, params.Per, params.Exp) + token, err := jwtHandler.GenerateJWT(1, params.Email, params.Role, params.Exp) if err != nil || token == "" { t.Fatal("should not error") } @@ -95,7 +95,7 @@ func TestGenerateClaims(t *testing.T) { func TestJWTHandlerInvalidateToken(t *testing.T) { jwtHandler := NewJWTHandler() - token, err := jwtHandler.GenerateJWT(params.ID, params.Email, params.Role, params.Per, params.Exp) + token, err := jwtHandler.GenerateJWT(params.ID, params.Email, params.Role, params.Exp) if err != nil { t.Error("error while generating token") } @@ -119,7 +119,7 @@ func TestJWTHandlerIsBlacklisted(t *testing.T) { jwtHandler := NewJWTHandler() cookie, err := jwtHandler.GenerateJWT(1000, helper.RandomEmail(), "example-role", - params.Per, time.Now().Add(1*time.Hour)) + time.Now().Add(1*time.Hour)) if err != nil { t.Error("generate cookie/token should not error") } @@ -151,7 +151,7 @@ func TestJWTHandlerIsBlacklisted(t *testing.T) { func TestJWTHandlerIsAuthenticated(t *testing.T) { jwtHandler := NewJWTHandler() - token, err := jwtHandler.GenerateJWT(params.ID, params.Email, params.Role, params.Per, params.Exp) + token, err := jwtHandler.GenerateJWT(params.ID, params.Email, params.Role, params.Exp) if err != nil { t.Error("error while generating token") } @@ -187,26 +187,9 @@ func TestJWTHandlerIsAuthenticated(t *testing.T) { }() } -func TestJWTHandlerHasPermission(t *testing.T) { - jwtHandler := NewJWTHandler() - token, err := jwtHandler.GenerateJWT(params.ID, params.Email, params.Role, params.Per, params.Exp) - if err != nil { - t.Error("Error while generating token:", err) - } - if token == "" { - t.Error("Error: Token is empty") - } - c := helper.NewFiberCtx() - c.Request().Header.Add(fiber.HeaderAuthorization, "Bearer "+token) - jwtHandler.HasPermission(c, 25) - if c.Response().Header.StatusCode() != fiber.StatusUnauthorized { - t.Error("Should authorized") - } -} - func TestJWTHandlerHasRole(t *testing.T) { jwtHandler := NewJWTHandler() - token, err := jwtHandler.GenerateJWT(params.ID, params.Email, params.Role, params.Per, params.Exp) + token, err := jwtHandler.GenerateJWT(params.ID, params.Email, params.Role, params.Exp) if err != nil { t.Error("Error while generating token:", err) } @@ -217,29 +200,24 @@ func TestJWTHandlerHasRole(t *testing.T) { c.Request().Header.Add(fiber.HeaderAuthorization, "Bearer "+token) jwtHandler.HasRole(c, "test-role") if c.Response().Header.StatusCode() != fiber.StatusUnauthorized { - t.Error(constants.Unauthorized) + t.Error(consts.Unauthorized) } } func TestJWTHandlerCheckHasPermission(t *testing.T) { jwtHandler := NewJWTHandler() - token, err := jwtHandler.GenerateJWT(params.ID, params.Email, params.Role, params.Per, params.Exp) + token, err := jwtHandler.GenerateJWT(params.ID, params.Email, params.Role, params.Exp) if err != nil { t.Error("Error while generating token:", err) } if token == "" { t.Error("Error: Token is empty") } - - err2 := jwtHandler.CheckHasPermission(9999) - if err2 == nil { - t.Error(constants.Unauthorized) - } } func TestJWTHandlerCheckHasRole(t *testing.T) { jwtHandler := NewJWTHandler() - token, err := jwtHandler.GenerateJWT(params.ID, params.Email, params.Role, params.Per, params.Exp) + token, err := jwtHandler.GenerateJWT(params.ID, params.Email, params.Role, params.Exp) if err != nil { t.Error("Error while generating token:", err) } @@ -247,9 +225,9 @@ func TestJWTHandlerCheckHasRole(t *testing.T) { t.Error("Error: Token is empty") } - err2 := jwtHandler.CheckHasRole("permission-1") - if err2 == nil { - t.Error(constants.Unauthorized) + checkErr := jwtHandler.CheckHasRole("permission-1") + if checkErr == nil { + t.Error(consts.Unauthorized) } } diff --git a/internal/response/response.go b/internal/response/response.go index eb7447d..826525a 100644 --- a/internal/response/response.go +++ b/internal/response/response.go @@ -3,23 +3,26 @@ package response import ( "strings" + "github.com/Lukmanern/gost/internal/consts" "github.com/gofiber/fiber/v2" ) -// Response struct is standart JSON structure that this project used. -// You can change if you want. This also prevents developers make -// common mistake to make messsage to frontend developer. type Response struct { Message string `json:"message"` Success bool `json:"success"` Data interface{} `json:"data"` } -const ( - MessageSuccessCreated = "data successfully created" - MessageSuccessLoaded = "data successfully loaded" - MessageUnauthorized = "unauthorized" -) +// CreateResponse generates a new response +// with the given parameters. +func CreateResponse(c *fiber.Ctx, statusCode int, response Response) error { + c.Status(statusCode) + return c.JSON(Response{ + Message: strings.ToLower(response.Message), + Success: response.Success, + Data: response.Data, + }) +} // SuccessNoContent formats a successful // response with HTTP status 204. @@ -28,56 +31,72 @@ func SuccessNoContent(c *fiber.Ctx) error { return c.Send(nil) } -// CreateResponse generates a new response -// with the given parameters. -func CreateResponse(c *fiber.Ctx, statusCode int, success bool, message string, data interface{}) error { - c.Status(statusCode) - return c.JSON(Response{ - Message: strings.ToLower(message), - Success: success, - Data: data, - }) -} - // SuccessLoaded formats a successful response // with HTTP status 200 and the provided data. func SuccessLoaded(c *fiber.Ctx, data interface{}) error { - return CreateResponse(c, fiber.StatusOK, true, MessageSuccessLoaded, data) + return CreateResponse(c, fiber.StatusOK, Response{ + Message: strings.ToLower(consts.SuccessLoaded), + Success: true, + Data: data, + }) } // SuccessCreated formats a successful response // with HTTP status 201 and the provided data. func SuccessCreated(c *fiber.Ctx, data interface{}) error { - return CreateResponse(c, fiber.StatusCreated, true, MessageSuccessCreated, data) + return CreateResponse(c, fiber.StatusCreated, Response{ + Message: strings.ToLower(consts.SuccessCreated), + Success: true, + Data: data, + }) } -// BadRequest formats a response with HTTP -// status 400 and the specified message. -func BadRequest(c *fiber.Ctx, message string) error { - return CreateResponse(c, fiber.StatusBadRequest, false, message, nil) +// BadRequest formats a response with HTTP status 400. +func BadRequest(c *fiber.Ctx) error { + return CreateResponse(c, fiber.StatusBadRequest, Response{ + Message: consts.BadRequest, + Success: false, + Data: nil, + }) } // Unauthorized formats a response with // HTTP status 401 indicating unauthorized access. func Unauthorized(c *fiber.Ctx) error { - return CreateResponse(c, fiber.StatusUnauthorized, false, MessageUnauthorized, nil) + return CreateResponse(c, fiber.StatusUnauthorized, Response{ + Message: consts.Unauthorized, + Success: false, + Data: nil, + }) } // DataNotFound formats a response with // HTTP status 404 and the specified message. -func DataNotFound(c *fiber.Ctx, message string) error { - return CreateResponse(c, fiber.StatusNotFound, false, message, nil) +func DataNotFound(c *fiber.Ctx) error { + return CreateResponse(c, fiber.StatusNotFound, Response{ + Message: consts.NotFound, + Success: false, + Data: nil, + }) } // Error formats an error response // with HTTP status 500 and the specified message. func Error(c *fiber.Ctx, message string) error { - return CreateResponse(c, fiber.StatusInternalServerError, false, message, nil) + return CreateResponse(c, fiber.StatusInternalServerError, Response{ + Message: message, + Success: false, + Data: nil, + }) } // ErrorWithData formats an error response // with HTTP status 500 and the specified // message and data. func ErrorWithData(c *fiber.Ctx, message string, data interface{}) error { - return CreateResponse(c, fiber.StatusInternalServerError, false, message, data) + return CreateResponse(c, fiber.StatusInternalServerError, Response{ + Message: message, + Success: false, + Data: data, + }) } diff --git a/internal/response/response_test.go b/internal/response/response_test.go index dc4700a..4679d99 100644 --- a/internal/response/response_test.go +++ b/internal/response/response_test.go @@ -71,7 +71,11 @@ func TestCreateResponse(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := CreateResponse(tt.args.c, tt.args.statusCode, tt.args.success, tt.args.message, tt.args.data); (err != nil) != tt.wantErr { + if err := CreateResponse(tt.args.c, tt.args.statusCode, Response{ + Message: tt.args.message, + Success: tt.args.success, + Data: tt.args.data, + }); (err != nil) != tt.wantErr { t.Errorf("CreateResponse() error = %v, wantErr %v", err, tt.wantErr) } }) @@ -181,7 +185,7 @@ func TestBadRequest(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := BadRequest(tt.args.c, tt.args.message); (err != nil) != tt.wantErr { + if err := BadRequest(tt.args.c); (err != nil) != tt.wantErr { t.Errorf("BadRequest() error = %v, wantErr %v", err, tt.wantErr) } }) @@ -244,7 +248,7 @@ func TestDataNotFound(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := DataNotFound(tt.args.c, tt.args.message); (err != nil) != tt.wantErr { + if err := DataNotFound(tt.args.c); (err != nil) != tt.wantErr { t.Errorf("DataNotFound() error = %v, wantErr %v", err, tt.wantErr) } })