From dc655e643741d97d8a58c4105c1ed48a61c730cc Mon Sep 17 00:00:00 2001 From: martinyonatann Date: Tue, 17 Oct 2023 18:27:49 +0700 Subject: [PATCH] feat: login and detail endpoint --- config/config-local.yaml.example | 6 +- config/config.go | 4 +- .../20230924142159_add_user_table.sql | 6 +- internal/users/delivery/http/v1/handler.go | 133 +++++++++--------- internal/users/delivery/http/v1/route.go | 2 + internal/users/dtos/update_users.go | 36 ----- .../dtos/{create_users.go => user_create.go} | 8 +- internal/users/dtos/user_detail.go | 16 +++ internal/users/dtos/user_login.go | 24 ++++ internal/users/dtos/user_update.go | 25 ++++ ...e_user_status.go => user_update_status.go} | 0 .../{create_users.go => user_create.go} | 0 internal/users/entities/user_detail.go | 16 +++ internal/users/entities/user_identity.go | 6 + .../{update_users.go => user_update.go} | 10 +- ...e_user_status.go => user_update_status.go} | 0 internal/users/entities/users.go | 25 ++-- internal/users/mock/repository_mock.go | 29 ++++ internal/users/mock/usecase_mock.go | 75 +++++++--- internal/users/repository/repository.go | 59 +++++++- .../repository_query/repository_query.go | 4 + .../users/check_is_user_exist.sql | 4 + .../users/get_detail_by_email.sql | 6 + .../repository_query/users/insert.sql | 6 +- .../repository_query/users/select.sql | 4 +- internal/users/repository/repository_test.go | 3 +- internal/users/usecase/usecase.go | 84 ++++++++--- internal/users/usecase/usecase_test.go | 33 +++-- pkg/apperror/errors.go | 10 ++ pkg/constant/constant.go | 19 +++ pkg/middleware/auth.go | 23 ++- pkg/utils/crypto.go | 79 +++++++++++ .../dtos => pkg/utils/response}/response.go | 2 +- 33 files changed, 550 insertions(+), 207 deletions(-) delete mode 100644 internal/users/dtos/update_users.go rename internal/users/dtos/{create_users.go => user_create.go} (65%) create mode 100644 internal/users/dtos/user_detail.go create mode 100644 internal/users/dtos/user_login.go create mode 100644 internal/users/dtos/user_update.go rename internal/users/dtos/{update_user_status.go => user_update_status.go} (100%) rename internal/users/entities/{create_users.go => user_create.go} (100%) create mode 100644 internal/users/entities/user_detail.go create mode 100644 internal/users/entities/user_identity.go rename internal/users/entities/{update_users.go => user_update.go} (65%) rename internal/users/entities/{update_user_status.go => user_update_status.go} (100%) create mode 100644 internal/users/repository/repository_query/users/check_is_user_exist.sql create mode 100644 internal/users/repository/repository_query/users/get_detail_by_email.sql create mode 100644 pkg/apperror/errors.go create mode 100644 pkg/constant/constant.go create mode 100644 pkg/utils/crypto.go rename {internal/users/dtos => pkg/utils/response}/response.go (98%) diff --git a/config/config-local.yaml.example b/config/config-local.yaml.example index 319ac42..78e6ccb 100644 --- a/config/config-local.yaml.example +++ b/config/config-local.yaml.example @@ -11,6 +11,10 @@ server: database: host: localhost port: 3306 - name: golang-clean-architecture + name: users user: mysql password: pwd + +authentication: + key: DoWithLogic!@# + secret_key: s3cr#tK3y!@# diff --git a/config/config.go b/config/config.go index e24746c..b96c26f 100644 --- a/config/config.go +++ b/config/config.go @@ -34,7 +34,9 @@ type ( } AuthenticationConfig struct { - Key string `mapstructure:"key"` + Key string `mapstructure:"key"` + SecretKey string `mapstructure:"secret_key"` + SaltKey string `mapstructure:"salt_key"` } ) diff --git a/database/mysql/migration/20230924142159_add_user_table.sql b/database/mysql/migration/20230924142159_add_user_table.sql index 4a037e8..b6f6ced 100644 --- a/database/mysql/migration/20230924142159_add_user_table.sql +++ b/database/mysql/migration/20230924142159_add_user_table.sql @@ -2,16 +2,18 @@ -- +goose StatementBegin CREATE TABLE `users` ( `id` int NOT NULL AUTO_INCREMENT, + `email` varchar(255) NOT NULL, + `password` text NOT NULL, `fullname` varchar(255) NOT NULL, `phone_number` varchar(15) NOT NULL, `user_type` varchar(50) NOT NULL, `is_active` tinyint(1) NOT NULL, `created_at` timestamp NOT NULL, `created_by` varchar(255) NOT NULL, - `updated_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp DEFAULT NULL, `updated_by` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -- +goose StatementEnd diff --git a/internal/users/delivery/http/v1/handler.go b/internal/users/delivery/http/v1/handler.go index f48d415..41416f3 100644 --- a/internal/users/delivery/http/v1/handler.go +++ b/internal/users/delivery/http/v1/handler.go @@ -10,13 +10,17 @@ import ( "github.com/DoWithLogic/golang-clean-architecture/internal/users/dtos" "github.com/DoWithLogic/golang-clean-architecture/internal/users/entities" usecases "github.com/DoWithLogic/golang-clean-architecture/internal/users/usecase" + "github.com/DoWithLogic/golang-clean-architecture/pkg/middleware" "github.com/DoWithLogic/golang-clean-architecture/pkg/otel/zerolog" + "github.com/DoWithLogic/golang-clean-architecture/pkg/utils/response" "github.com/labstack/echo/v4" ) type ( Handlers interface { + Login(c echo.Context) error CreateUser(c echo.Context) error + UserDetail(c echo.Context) error UpdateUser(c echo.Context) error UpdateUserStatus(c echo.Context) error } @@ -40,19 +44,40 @@ func NewHandlers(uc usecases.Usecase, log *zerolog.Logger) Handlers { return &handlers{uc, log} } +func (h *handlers) Login(c echo.Context) error { + var ( + request dtos.UserLoginRequest + ) + + if err := c.Bind(&request); err != nil { + return c.JSON(http.StatusBadRequest, response.NewResponseError(http.StatusBadRequest, response.MsgFailed, err.Error())) + } + + if err := request.Validate(); err != nil { + return c.JSON(http.StatusBadRequest, response.NewResponseError(http.StatusBadRequest, response.MsgFailed, err.Error())) + } + + authData, httpCode, err := h.uc.Login(c.Request().Context(), request) + if err != nil { + return c.JSON(httpCode, response.NewResponseError(httpCode, response.MsgFailed, err.Error())) + } + + return c.JSON(httpCode, response.NewResponse(httpCode, response.MsgSuccess, authData)) +} + func (h *handlers) CreateUser(c echo.Context) error { var ( ctx, cancel = context.WithTimeout(c.Request().Context(), time.Duration(30*time.Second)) - payload dtos.CreateUserPayload + payload dtos.CreateUserRequest ) defer cancel() if err := c.Bind(&payload); err != nil { h.log.Z().Err(err).Msg("[handlers]CreateUser.Bind") - return c.JSON(http.StatusBadRequest, dtos.NewResponseError( + return c.JSON(http.StatusBadRequest, response.NewResponseError( http.StatusBadRequest, - dtos.MsgFailed, + response.MsgFailed, err.Error()), ) } @@ -60,87 +85,65 @@ func (h *handlers) CreateUser(c echo.Context) error { if err := payload.Validate(); err != nil { h.log.Z().Err(err).Msg("[handlers]CreateUser.Validate") - return c.JSON(http.StatusBadRequest, dtos.NewResponseError( + return c.JSON(http.StatusBadRequest, response.NewResponseError( http.StatusBadRequest, - dtos.MsgFailed, + response.MsgFailed, err.Error()), ) } - argsCreateUser := entities.CreateUser{ - FullName: payload.FullName, - PhoneNumber: payload.PhoneNumber, - } - - createdData, err := h.uc.CreateUser(ctx, argsCreateUser) + userID, httpCode, err := h.uc.Create(ctx, payload) if err != nil { - return c.JSON(http.StatusInternalServerError, dtos.NewResponseError( - http.StatusInternalServerError, - dtos.MsgFailed, + return c.JSON(httpCode, response.NewResponseError( + httpCode, + response.MsgFailed, err.Error()), ) } - return c.JSON(http.StatusOK, dtos.NewResponse(http.StatusOK, dtos.MsgSuccess, createdData)) + return c.JSON(http.StatusOK, response.NewResponse(http.StatusOK, response.MsgSuccess, map[string]int64{"id": userID})) +} + +func (h *handlers) UserDetail(c echo.Context) error { + ctx, cancel := context.WithTimeout(c.Request().Context(), time.Duration(30*time.Second)) + defer cancel() + + userID := c.Get("identity").(*middleware.CustomClaims).UserID + + data, code, err := h.uc.Detail(ctx, userID) + if err != nil { + return c.JSON(code, response.NewResponseError(code, response.MsgFailed, err.Error())) + } + + return c.JSON(code, response.NewResponse(code, response.MsgSuccess, data)) } func (h *handlers) UpdateUser(c echo.Context) error { var ( ctx, cancel = context.WithTimeout(c.Request().Context(), time.Duration(30*time.Second)) - payload dtos.UpdateUserPayload + request dtos.UpdateUserRequest ) defer cancel() - h.log.Z().Info().Msg("[handlers]UpdateUser") - - userID, err := strconv.ParseInt(c.Param("id"), 10, 64) - if err != nil { - h.log.Z().Err(err).Msg("[handlers]UpdateUser.ParseParam") - - return c.JSON(http.StatusBadRequest, dtos.NewResponseError( - http.StatusBadRequest, - dtos.MsgFailed, - err.Error()), - ) - } + request.UserID = c.Get("identity").(entities.Identity).UserID - if err := c.Bind(&payload); err != nil { + if err := c.Bind(&request); err != nil { h.log.Z().Err(err).Msg("[handlers]UpdateUser.Bind") - return c.JSON(http.StatusBadRequest, dtos.NewResponseError( - http.StatusBadRequest, - dtos.MsgFailed, - err.Error()), - ) + return c.JSON(http.StatusBadRequest, response.NewResponseError(http.StatusBadRequest, response.MsgFailed, err.Error())) } - if err := payload.Validate(); err != nil { + if err := request.Validate(); err != nil { h.log.Z().Err(err).Msg("[handlers]UpdateUser.Validate") - return c.JSON(http.StatusBadRequest, dtos.NewResponseError( - http.StatusBadRequest, - dtos.MsgFailed, - err.Error()), - ) + return c.JSON(http.StatusBadRequest, response.NewResponseError(http.StatusBadRequest, response.MsgFailed, err.Error())) } - argsUpdateUser := entities.UpdateUsers{ - UserID: userID, - Fullname: payload.Fullname, - PhoneNumber: payload.PhoneNumber, - UserType: payload.UserType, - } - - err = h.uc.UpdateUser(ctx, argsUpdateUser) - if err != nil { - return c.JSON(http.StatusBadRequest, dtos.NewResponseError( - http.StatusInternalServerError, - dtos.MsgFailed, - err.Error()), - ) + if err := h.uc.PartialUpdate(ctx, request); err != nil { + return c.JSON(http.StatusBadRequest, response.NewResponseError(http.StatusInternalServerError, response.MsgFailed, err.Error())) } - return c.JSON(http.StatusOK, dtos.NewResponse(http.StatusOK, dtos.MsgSuccess, nil)) + return c.JSON(http.StatusOK, response.NewResponse(http.StatusOK, response.MsgSuccess, nil)) } func (h *handlers) UpdateUserStatus(c echo.Context) error { @@ -154,9 +157,9 @@ func (h *handlers) UpdateUserStatus(c echo.Context) error { if err != nil { h.log.Z().Err(err).Msg("[handlers]UpdateUser.ParseParam") - return c.JSON(http.StatusBadRequest, dtos.NewResponseError( + return c.JSON(http.StatusBadRequest, response.NewResponseError( http.StatusBadRequest, - dtos.MsgFailed, + response.MsgFailed, err.Error()), ) } @@ -167,11 +170,7 @@ func (h *handlers) UpdateUserStatus(c echo.Context) error { case BooleanTextTrue: payload.IsActive = true default: - return c.JSON(http.StatusBadRequest, dtos.NewResponseError( - http.StatusBadRequest, - dtos.MsgFailed, - ErrInvalidIsActive.Error()), - ) + return c.JSON(http.StatusBadRequest, response.NewResponseError(http.StatusBadRequest, response.MsgFailed, ErrInvalidIsActive.Error())) } argsUpdateUserStatus := entities.UpdateUserStatus{ @@ -179,14 +178,10 @@ func (h *handlers) UpdateUserStatus(c echo.Context) error { IsActive: payload.IsActive, } - err = h.uc.UpdateUserStatus(ctx, argsUpdateUserStatus) + err = h.uc.UpdateStatus(ctx, argsUpdateUserStatus) if err != nil { - return c.JSON(http.StatusBadRequest, dtos.NewResponseError( - http.StatusInternalServerError, - dtos.MsgFailed, - err.Error()), - ) + return c.JSON(http.StatusBadRequest, response.NewResponseError(http.StatusInternalServerError, response.MsgFailed, err.Error())) } - return c.JSON(http.StatusOK, dtos.NewResponse(http.StatusOK, dtos.MsgSuccess, nil)) + return c.JSON(http.StatusOK, response.NewResponse(http.StatusOK, response.MsgSuccess, nil)) } diff --git a/internal/users/delivery/http/v1/route.go b/internal/users/delivery/http/v1/route.go index d5d415e..84f0b6b 100644 --- a/internal/users/delivery/http/v1/route.go +++ b/internal/users/delivery/http/v1/route.go @@ -9,6 +9,8 @@ import ( func UserPrivateRoute(version *echo.Group, h Handlers, cfg config.Config) { users := version.Group("users") users.POST("", h.CreateUser) + users.POST("/login", h.Login) + users.GET("/detail", h.UserDetail, middleware.AuthorizeJWT(cfg)) users.PATCH("/:id", h.UpdateUser, middleware.AuthorizeJWT(cfg)) users.PUT("/:id", h.UpdateUserStatus, middleware.AuthorizeJWT(cfg)) } diff --git a/internal/users/dtos/update_users.go b/internal/users/dtos/update_users.go deleted file mode 100644 index b94dbc0..0000000 --- a/internal/users/dtos/update_users.go +++ /dev/null @@ -1,36 +0,0 @@ -package dtos - -import ( - "errors" - - "github.com/DoWithLogic/golang-clean-architecture/internal/users/entities" - "github.com/invopop/validation" -) - -type UpdateUserPayload struct { - Fullname string `json:"fullname"` - PhoneNumber string `json:"phone_number"` - UserType string `json:"user_type"` -} - -var ( - ErrInvalidUserType = errors.New("invalid user_type") -) - -func (cup UpdateUserPayload) Validate() error { - var validationFields []*validation.FieldRules - - if cup.UserType != "" && (cup.UserType != entities.UserTypePremium && cup.UserType != entities.UserTypeRegular) { - return ErrInvalidUserType - } - - if cup.Fullname != "" { - validationFields = append(validationFields, validation.Field(&cup.Fullname, validation.Required, validation.Length(0, 50))) - } - - if cup.PhoneNumber != "" { - validationFields = append(validationFields, validation.Field(&cup.PhoneNumber, validation.Required, validation.Length(0, 13))) - } - - return validation.ValidateStruct(&cup, validationFields...) -} diff --git a/internal/users/dtos/create_users.go b/internal/users/dtos/user_create.go similarity index 65% rename from internal/users/dtos/create_users.go rename to internal/users/dtos/user_create.go index dddddd8..6eec1cb 100644 --- a/internal/users/dtos/create_users.go +++ b/internal/users/dtos/user_create.go @@ -4,9 +4,11 @@ import ( "github.com/invopop/validation" ) -type CreateUserPayload struct { +type CreateUserRequest struct { FullName string `json:"fullname"` PhoneNumber string `json:"phone_number"` + Email string `json:"email"` + Password string `json:"password"` } type CreateUserResponse struct { @@ -15,9 +17,11 @@ type CreateUserResponse struct { ExpiredAt int64 `json:"expired_at"` } -func (cup CreateUserPayload) Validate() error { +func (cup CreateUserRequest) Validate() error { return validation.ValidateStruct(&cup, validation.Field(&cup.FullName, validation.Required, validation.Length(0, 50)), validation.Field(&cup.PhoneNumber, validation.Required, validation.Length(0, 13)), + validation.Field(&cup.Email, validation.Required), + validation.Field(&cup.Password, validation.Required), ) } diff --git a/internal/users/dtos/user_detail.go b/internal/users/dtos/user_detail.go new file mode 100644 index 0000000..167c95c --- /dev/null +++ b/internal/users/dtos/user_detail.go @@ -0,0 +1,16 @@ +package dtos + +import "time" + +type ( + UserDetailResponse struct { + UserID int64 `json:"id"` + Email string `json:"email"` + Fullname string `json:"fullname"` + PhoneNumber string `json:"phone_number"` + UserType string `json:"user_type"` + IsActive bool `json:"is_active"` + CreatedAt time.Time `json:"created_at"` + CreatedBy string `json:"created_by"` + } +) diff --git a/internal/users/dtos/user_login.go b/internal/users/dtos/user_login.go new file mode 100644 index 0000000..95df929 --- /dev/null +++ b/internal/users/dtos/user_login.go @@ -0,0 +1,24 @@ +package dtos + +import ( + "github.com/invopop/validation" +) + +type ( + UserLoginRequest struct { + Email string `json:"email"` + Password string `json:"password"` + } + + UserLoginResponse struct { + AccessToken string `json:"access_token"` + ExpiredAt int64 `json:"expired_at"` + } +) + +func (ulr UserLoginRequest) Validate() error { + return validation.ValidateStruct(&ulr, + validation.Field(&ulr.Email, validation.Required), + validation.Field(&ulr.Password, validation.Required), + ) +} diff --git a/internal/users/dtos/user_update.go b/internal/users/dtos/user_update.go new file mode 100644 index 0000000..f80e592 --- /dev/null +++ b/internal/users/dtos/user_update.go @@ -0,0 +1,25 @@ +package dtos + +import ( + "github.com/DoWithLogic/golang-clean-architecture/pkg/apperror" + "github.com/DoWithLogic/golang-clean-architecture/pkg/constant" +) + +type UpdateUserRequest struct { + UserID int64 `json:"-"` + Fullname string `json:"fullname"` + PhoneNumber string `json:"phone_number"` + UserType string `json:"user_type"` + Email string `json:"email"` + Password string `json:"password"` +} + +var () + +func (cup UpdateUserRequest) Validate() error { + if cup.UserType != "" && cup.UserType != constant.UserTypePremium && cup.UserType != constant.UserTypeRegular { + return apperror.ErrInvalidUserType + } + + return nil +} diff --git a/internal/users/dtos/update_user_status.go b/internal/users/dtos/user_update_status.go similarity index 100% rename from internal/users/dtos/update_user_status.go rename to internal/users/dtos/user_update_status.go diff --git a/internal/users/entities/create_users.go b/internal/users/entities/user_create.go similarity index 100% rename from internal/users/entities/create_users.go rename to internal/users/entities/user_create.go diff --git a/internal/users/entities/user_detail.go b/internal/users/entities/user_detail.go new file mode 100644 index 0000000..2243608 --- /dev/null +++ b/internal/users/entities/user_detail.go @@ -0,0 +1,16 @@ +package entities + +import "github.com/DoWithLogic/golang-clean-architecture/internal/users/dtos" + +func NewUserDetail(data Users) dtos.UserDetailResponse { + return dtos.UserDetailResponse{ + UserID: data.UserID, + Email: data.Email, + Fullname: data.Fullname, + PhoneNumber: data.PhoneNumber, + UserType: data.UserType, + IsActive: data.IsActive, + CreatedAt: data.CreatedAt, + CreatedBy: data.CreatedBy, + } +} diff --git a/internal/users/entities/user_identity.go b/internal/users/entities/user_identity.go new file mode 100644 index 0000000..b574d86 --- /dev/null +++ b/internal/users/entities/user_identity.go @@ -0,0 +1,6 @@ +package entities + +type Identity struct { + Email string `json:"email,omitempty"` + UserID int64 `json:"user_id,omitempty"` +} diff --git a/internal/users/entities/update_users.go b/internal/users/entities/user_update.go similarity index 65% rename from internal/users/entities/update_users.go rename to internal/users/entities/user_update.go index 08235ab..4b6a43e 100644 --- a/internal/users/entities/update_users.go +++ b/internal/users/entities/user_update.go @@ -1,17 +1,23 @@ package entities -import "time" +import ( + "time" + + "github.com/DoWithLogic/golang-clean-architecture/internal/users/dtos" +) type UpdateUsers struct { UserID int64 Fullname string + Email string + Password string PhoneNumber string UserType string UpdatedAt time.Time UpdatedBy string } -func NewUpdateUsers(data UpdateUsers) UpdateUsers { +func NewUpdateUsers(data dtos.UpdateUserRequest) UpdateUsers { return UpdateUsers{ UserID: data.UserID, Fullname: data.Fullname, diff --git a/internal/users/entities/update_user_status.go b/internal/users/entities/user_update_status.go similarity index 100% rename from internal/users/entities/update_user_status.go rename to internal/users/entities/user_update_status.go diff --git a/internal/users/entities/users.go b/internal/users/entities/users.go index dc5d1db..fbc7994 100644 --- a/internal/users/entities/users.go +++ b/internal/users/entities/users.go @@ -1,13 +1,19 @@ package entities import ( - "errors" "time" + + "github.com/DoWithLogic/golang-clean-architecture/config" + "github.com/DoWithLogic/golang-clean-architecture/internal/users/dtos" + "github.com/DoWithLogic/golang-clean-architecture/pkg/constant" + "github.com/DoWithLogic/golang-clean-architecture/pkg/utils" ) type ( Users struct { UserID int64 + Email string + Password string Fullname string PhoneNumber string UserType string @@ -23,22 +29,15 @@ type ( } ) -const ( - UserTypeRegular = "regular_user" - UserTypePremium = "premium_user" -) - -var ( - ErrInvalidLockOpt = errors.New("can't do lock with multiple type") -) - -func NewCreateUser(data CreateUser) Users { +func NewCreateUser(data dtos.CreateUserRequest, cfg config.Config) Users { return Users{ Fullname: data.FullName, + Email: data.Email, + Password: utils.Encrypt(data.Password, cfg), PhoneNumber: data.PhoneNumber, - UserType: UserTypeRegular, + UserType: constant.UserTypeRegular, IsActive: true, CreatedAt: time.Now(), - CreatedBy: "martin", + CreatedBy: constant.UserSystem, } } diff --git a/internal/users/mock/repository_mock.go b/internal/users/mock/repository_mock.go index b04ffc1..a2bd439 100644 --- a/internal/users/mock/repository_mock.go +++ b/internal/users/mock/repository_mock.go @@ -51,6 +51,21 @@ func (mr *MockRepositoryMockRecorder) Atomic(ctx, opt, repo interface{}) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Atomic", reflect.TypeOf((*MockRepository)(nil).Atomic), ctx, opt, repo) } +// GetUserByEmail mocks base method. +func (m *MockRepository) GetUserByEmail(arg0 context.Context, arg1 string) (entities.Users, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserByEmail", arg0, arg1) + ret0, _ := ret[0].(entities.Users) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUserByEmail indicates an expected call of GetUserByEmail. +func (mr *MockRepositoryMockRecorder) GetUserByEmail(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByEmail", reflect.TypeOf((*MockRepository)(nil).GetUserByEmail), arg0, arg1) +} + // GetUserByID mocks base method. func (m *MockRepository) GetUserByID(arg0 context.Context, arg1 int64, arg2 ...entities.LockingOpt) (entities.Users, error) { m.ctrl.T.Helper() @@ -71,6 +86,20 @@ func (mr *MockRepositoryMockRecorder) GetUserByID(arg0, arg1 interface{}, arg2 . return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByID", reflect.TypeOf((*MockRepository)(nil).GetUserByID), varargs...) } +// IsUserExist mocks base method. +func (m *MockRepository) IsUserExist(ctx context.Context, email string) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsUserExist", ctx, email) + ret0, _ := ret[0].(bool) + return ret0 +} + +// IsUserExist indicates an expected call of IsUserExist. +func (mr *MockRepositoryMockRecorder) IsUserExist(ctx, email interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsUserExist", reflect.TypeOf((*MockRepository)(nil).IsUserExist), ctx, email) +} + // SaveNewUser mocks base method. func (m *MockRepository) SaveNewUser(arg0 context.Context, arg1 entities.Users) (int64, error) { m.ctrl.T.Helper() diff --git a/internal/users/mock/usecase_mock.go b/internal/users/mock/usecase_mock.go index 8a42d80..fb51c91 100644 --- a/internal/users/mock/usecase_mock.go +++ b/internal/users/mock/usecase_mock.go @@ -36,45 +36,78 @@ func (m *MockUsecase) EXPECT() *MockUsecaseMockRecorder { return m.recorder } -// CreateUser mocks base method. -func (m *MockUsecase) CreateUser(ctx context.Context, user entities.CreateUser) (dtos.CreateUserResponse, error) { +// Create mocks base method. +func (m *MockUsecase) Create(ctx context.Context, payload dtos.CreateUserRequest) (int64, int, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateUser", ctx, user) - ret0, _ := ret[0].(dtos.CreateUserResponse) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret := m.ctrl.Call(m, "Create", ctx, payload) + ret0, _ := ret[0].(int64) + ret1, _ := ret[1].(int) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 } -// CreateUser indicates an expected call of CreateUser. -func (mr *MockUsecaseMockRecorder) CreateUser(ctx, user interface{}) *gomock.Call { +// Create indicates an expected call of Create. +func (mr *MockUsecaseMockRecorder) Create(ctx, payload interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUser", reflect.TypeOf((*MockUsecase)(nil).CreateUser), ctx, user) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockUsecase)(nil).Create), ctx, payload) } -// UpdateUser mocks base method. -func (m *MockUsecase) UpdateUser(ctx context.Context, updateData entities.UpdateUsers) error { +// Detail mocks base method. +func (m *MockUsecase) Detail(ctx context.Context, id int64) (dtos.UserDetailResponse, int, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateUser", ctx, updateData) + ret := m.ctrl.Call(m, "Detail", ctx, id) + ret0, _ := ret[0].(dtos.UserDetailResponse) + ret1, _ := ret[1].(int) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// Detail indicates an expected call of Detail. +func (mr *MockUsecaseMockRecorder) Detail(ctx, id interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Detail", reflect.TypeOf((*MockUsecase)(nil).Detail), ctx, id) +} + +// Login mocks base method. +func (m *MockUsecase) Login(ctx context.Context, request dtos.UserLoginRequest) (dtos.UserLoginResponse, int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Login", ctx, request) + ret0, _ := ret[0].(dtos.UserLoginResponse) + ret1, _ := ret[1].(int) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// Login indicates an expected call of Login. +func (mr *MockUsecaseMockRecorder) Login(ctx, request interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Login", reflect.TypeOf((*MockUsecase)(nil).Login), ctx, request) +} + +// PartialUpdate mocks base method. +func (m *MockUsecase) PartialUpdate(ctx context.Context, data dtos.UpdateUserRequest) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PartialUpdate", ctx, data) ret0, _ := ret[0].(error) return ret0 } -// UpdateUser indicates an expected call of UpdateUser. -func (mr *MockUsecaseMockRecorder) UpdateUser(ctx, updateData interface{}) *gomock.Call { +// PartialUpdate indicates an expected call of PartialUpdate. +func (mr *MockUsecaseMockRecorder) PartialUpdate(ctx, data interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUser", reflect.TypeOf((*MockUsecase)(nil).UpdateUser), ctx, updateData) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PartialUpdate", reflect.TypeOf((*MockUsecase)(nil).PartialUpdate), ctx, data) } -// UpdateUserStatus mocks base method. -func (m *MockUsecase) UpdateUserStatus(ctx context.Context, req entities.UpdateUserStatus) error { +// UpdateStatus mocks base method. +func (m *MockUsecase) UpdateStatus(ctx context.Context, req entities.UpdateUserStatus) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateUserStatus", ctx, req) + ret := m.ctrl.Call(m, "UpdateStatus", ctx, req) ret0, _ := ret[0].(error) return ret0 } -// UpdateUserStatus indicates an expected call of UpdateUserStatus. -func (mr *MockUsecaseMockRecorder) UpdateUserStatus(ctx, req interface{}) *gomock.Call { +// UpdateStatus indicates an expected call of UpdateStatus. +func (mr *MockUsecaseMockRecorder) UpdateStatus(ctx, req interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserStatus", reflect.TypeOf((*MockUsecase)(nil).UpdateUserStatus), ctx, req) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateStatus", reflect.TypeOf((*MockUsecase)(nil).UpdateStatus), ctx, req) } diff --git a/internal/users/repository/repository.go b/internal/users/repository/repository.go index e1fe071..9574d33 100644 --- a/internal/users/repository/repository.go +++ b/internal/users/repository/repository.go @@ -16,10 +16,12 @@ type ( Repository interface { Atomic(ctx context.Context, opt *sql.TxOptions, repo func(tx Repository) error) error + GetUserByID(context.Context, int64, ...entities.LockingOpt) (entities.Users, error) + GetUserByEmail(context.Context, string) (entities.Users, error) SaveNewUser(context.Context, entities.Users) (int64, error) UpdateUserByID(context.Context, entities.UpdateUsers) error - GetUserByID(context.Context, int64, ...entities.LockingOpt) (entities.Users, error) UpdateUserStatusByID(context.Context, entities.UpdateUserStatus) error + IsUserExist(ctx context.Context, email string) bool } repository struct { @@ -53,25 +55,26 @@ func (r *repository) Atomic(ctx context.Context, opt *sql.TxOptions, repo func(t return nil } -func (repo *repository) SaveNewUser(ctx context.Context, user entities.Users) (int64, error) { +func (repo *repository) SaveNewUser(ctx context.Context, user entities.Users) (userID int64, err error) { args := utils.Array{ + user.Email, + user.Password, user.Fullname, user.PhoneNumber, - user.IsActive, user.UserType, + user.IsActive, user.CreatedAt, user.CreatedBy, } - var userID int64 - err := new(datasource.DataSource).ExecSQL(repo.conn.ExecContext(ctx, repository_query.InsertUsers, args...)).Scan(nil, &userID) + err = new(datasource.DataSource).ExecSQL(repo.conn.ExecContext(ctx, repository_query.InsertUsers, args...)).Scan(nil, &userID) if err != nil { repo.log.Z().Err(err).Msg("[repository]SaveNewUser.ExecContext") return userID, err } - return userID, err + return userID, nil } func (repo *repository) UpdateUserByID(ctx context.Context, user entities.UpdateUsers) error { @@ -102,11 +105,13 @@ func (repo *repository) GetUserByID(ctx context.Context, userID int64, options . row := func(idx int) utils.Array { return utils.Array{ &userData.UserID, + &userData.Email, &userData.Fullname, &userData.PhoneNumber, &userData.UserType, &userData.IsActive, &userData.CreatedAt, + &userData.CreatedBy, } } @@ -142,3 +147,45 @@ func (repo *repository) UpdateUserStatusByID(ctx context.Context, req entities.U return nil } + +func (repo *repository) IsUserExist(ctx context.Context, email string) bool { + args := utils.Array{email} + + var id int64 + row := func(idx int) utils.Array { + return utils.Array{ + &id, + } + } + + err := new(datasource.DataSource).QuerySQL(repo.conn.QueryContext(ctx, repository_query.IsUserExist, args...)).Scan(row) + if err != nil { + repo.log.Z().Err(err).Msg("[repository]IsUserExist.QueryContext") + + return false + } + + return id != 0 +} + +func (repo *repository) GetUserByEmail(ctx context.Context, email string) (userDetail entities.Users, err error) { + args := utils.Array{ + email, + } + + row := func(idx int) utils.Array { + return utils.Array{ + &userDetail.UserID, + &userDetail.Email, + &userDetail.Password, + } + } + + err = new(datasource.DataSource).QuerySQL(repo.conn.QueryContext(ctx, repository_query.GetUserByEmail, args...)).Scan(row) + if err != nil { + repo.log.Z().Err(err).Msg("[repository]GetUserByID.QueryContext") + return entities.Users{}, err + } + + return userDetail, err +} diff --git a/internal/users/repository/repository_query/repository_query.go b/internal/users/repository/repository_query/repository_query.go index 49ff425..0469b8b 100644 --- a/internal/users/repository/repository_query/repository_query.go +++ b/internal/users/repository/repository_query/repository_query.go @@ -11,4 +11,8 @@ var ( UpdateUserStatusByID string //go:embed users/select.sql GetUserByID string + //go:embed users/get_detail_by_email.sql + GetUserByEmail string + //go:embed users/check_is_user_exist.sql + IsUserExist string ) diff --git a/internal/users/repository/repository_query/users/check_is_user_exist.sql b/internal/users/repository/repository_query/users/check_is_user_exist.sql new file mode 100644 index 0000000..e8ef448 --- /dev/null +++ b/internal/users/repository/repository_query/users/check_is_user_exist.sql @@ -0,0 +1,4 @@ +SELECT + u.id +FROM users u +WHERE u.email = ? \ No newline at end of file diff --git a/internal/users/repository/repository_query/users/get_detail_by_email.sql b/internal/users/repository/repository_query/users/get_detail_by_email.sql new file mode 100644 index 0000000..1ede9c3 --- /dev/null +++ b/internal/users/repository/repository_query/users/get_detail_by_email.sql @@ -0,0 +1,6 @@ +SELECT + u.id, + u.email, + u.password +FROM users u +WHERE u.email = ? \ No newline at end of file diff --git a/internal/users/repository/repository_query/users/insert.sql b/internal/users/repository/repository_query/users/insert.sql index e9228ec..a34c356 100644 --- a/internal/users/repository/repository_query/users/insert.sql +++ b/internal/users/repository/repository_query/users/insert.sql @@ -1,8 +1,10 @@ INSERT INTO users ( + email, + password, fullname, phone_number, - is_active, user_type, + is_active, created_at, created_by -)VALUE (?, ?, ?, ?, ?, ?) \ No newline at end of file +)VALUE (?, ?, ?, ?, ?, ?, ?, ?) \ No newline at end of file diff --git a/internal/users/repository/repository_query/users/select.sql b/internal/users/repository/repository_query/users/select.sql index 2e5d1f6..ace5edd 100644 --- a/internal/users/repository/repository_query/users/select.sql +++ b/internal/users/repository/repository_query/users/select.sql @@ -1,9 +1,11 @@ SELECT u.id, + u.email, u.fullname, u.phone_number, u.user_type, u.is_active, - u.created_at + u.created_at, + u.created_by FROM users u WHERE u.id = ? diff --git a/internal/users/repository/repository_test.go b/internal/users/repository/repository_test.go index 3003d64..823fcb1 100644 --- a/internal/users/repository/repository_test.go +++ b/internal/users/repository/repository_test.go @@ -10,6 +10,7 @@ import ( "github.com/DoWithLogic/golang-clean-architecture/internal/users/entities" "github.com/DoWithLogic/golang-clean-architecture/internal/users/repository" "github.com/DoWithLogic/golang-clean-architecture/internal/users/repository/repository_query" + "github.com/DoWithLogic/golang-clean-architecture/pkg/constant" "github.com/DoWithLogic/golang-clean-architecture/pkg/otel/zerolog" "github.com/jmoiron/sqlx" "github.com/stretchr/testify/assert" @@ -36,7 +37,7 @@ func Test_repository_UpdateUserByID(t *testing.T) { user := entities.Users{ Fullname: "martin yonatan pasaribu", PhoneNumber: "08121213131414", - UserType: entities.UserTypePremium, + UserType: constant.UserTypePremium, IsActive: true, CreatedAt: currentTime, CreatedBy: "admin", diff --git a/internal/users/usecase/usecase.go b/internal/users/usecase/usecase.go index 945a063..e67e0d0 100644 --- a/internal/users/usecase/usecase.go +++ b/internal/users/usecase/usecase.go @@ -3,21 +3,28 @@ package usecase import ( "context" "database/sql" + "net/http" + "strings" "time" "github.com/DoWithLogic/golang-clean-architecture/config" "github.com/DoWithLogic/golang-clean-architecture/internal/users/dtos" "github.com/DoWithLogic/golang-clean-architecture/internal/users/entities" "github.com/DoWithLogic/golang-clean-architecture/internal/users/repository" + "github.com/DoWithLogic/golang-clean-architecture/pkg/apperror" "github.com/DoWithLogic/golang-clean-architecture/pkg/middleware" "github.com/DoWithLogic/golang-clean-architecture/pkg/otel/zerolog" + "github.com/DoWithLogic/golang-clean-architecture/pkg/utils" + "github.com/dgrijalva/jwt-go" ) type ( Usecase interface { - CreateUser(ctx context.Context, user entities.CreateUser) (dtos.CreateUserResponse, error) - UpdateUser(ctx context.Context, updateData entities.UpdateUsers) error - UpdateUserStatus(ctx context.Context, req entities.UpdateUserStatus) error + Login(ctx context.Context, request dtos.UserLoginRequest) (response dtos.UserLoginResponse, httpCode int, err error) + Create(ctx context.Context, payload dtos.CreateUserRequest) (userID int64, httpCode int, err error) + PartialUpdate(ctx context.Context, data dtos.UpdateUserRequest) error + UpdateStatus(ctx context.Context, req entities.UpdateUserStatus) error + Detail(ctx context.Context, id int64) (detail dtos.UserDetailResponse, httpCode int, err error) } usecase struct { @@ -31,42 +38,70 @@ func NewUseCase(repo repository.Repository, log *zerolog.Logger, cfg config.Conf return &usecase{repo, log, cfg} } -func (uc *usecase) CreateUser(ctx context.Context, payload entities.CreateUser) (dtos.CreateUserResponse, error) { - userID, err := uc.repo.SaveNewUser(ctx, entities.NewCreateUser(payload)) +func (uc *usecase) Login(ctx context.Context, request dtos.UserLoginRequest) (response dtos.UserLoginResponse, httpCode int, err error) { + dataLogin, err := uc.repo.GetUserByEmail(ctx, request.Email) if err != nil { - uc.log.Z().Err(err).Msg("[usecase]CreateUser.SaveNewUser") + return response, http.StatusInternalServerError, err + } + + uc.log.Z().Err(nil).Str("decrypted_password", dataLogin.Password).Str("password", request.Password).Msg("[Login]") - return dtos.CreateUserResponse{}, err + if !strings.EqualFold(utils.Decrypt(dataLogin.Password, uc.cfg), request.Password) { + return response, http.StatusUnauthorized, apperror.ErrInvalidPassword } expiredAt := time.Now().Add(time.Minute * 15).Unix() - token, err := middleware.GenerateJWT(userID, expiredAt, uc.cfg.Authentication.Key) + identityData := middleware.CustomClaims{ + UserID: dataLogin.UserID, + Email: dataLogin.Email, + StandardClaims: jwt.StandardClaims{ + ExpiresAt: expiredAt, + }, + } + + token, err := middleware.GenerateJWT(identityData, uc.cfg.Authentication.Key) if err != nil { - uc.log.Z().Err(err).Msg("[usecase]CreateUser.GenerateJWT") + return response, http.StatusInternalServerError, apperror.ErrFailedGenerateJWT + } + + response = dtos.UserLoginResponse{ + AccessToken: token, + ExpiredAt: expiredAt, + } + + return response, http.StatusOK, nil +} - return dtos.CreateUserResponse{}, err +func (uc *usecase) Create(ctx context.Context, payload dtos.CreateUserRequest) (userID int64, httpCode int, err error) { + if exist := uc.repo.IsUserExist(ctx, payload.Email); exist { + return userID, http.StatusConflict, apperror.ErrEmailAlreadyExist } - response := dtos.CreateUserResponse{ - UserID: userID, - Token: token, - ExpiredAt: expiredAt, + userID, err = uc.repo.SaveNewUser(ctx, entities.NewCreateUser(payload, uc.cfg)) + if err != nil { + uc.log.Z().Err(err).Msg("[usecase]CreateUser.SaveNewUser") + + return userID, http.StatusInternalServerError, err } - return response, nil + return userID, http.StatusOK, nil } -func (uc *usecase) UpdateUser(ctx context.Context, updateData entities.UpdateUsers) error { +func (uc *usecase) PartialUpdate(ctx context.Context, data dtos.UpdateUserRequest) error { return uc.repo.Atomic(ctx, &sql.TxOptions{}, func(tx repository.Repository) error { - - if _, err := tx.GetUserByID(ctx, updateData.UserID, entities.LockingOpt{PessimisticLocking: true}); err != nil { + opt := entities.LockingOpt{ + PessimisticLocking: true, + } + _, err := tx.GetUserByID(ctx, data.UserID, opt) + if err != nil { uc.log.Z().Err(err).Msg("[usecase]UpdateUser.GetUserByID") return err } - if err := tx.UpdateUserByID(ctx, entities.NewUpdateUsers(updateData)); err != nil { + err = tx.UpdateUserByID(ctx, entities.NewUpdateUsers(data)) + if err != nil { uc.log.Z().Err(err).Msg("[usecase]UpdateUser.UpdateUserByID") return err @@ -76,7 +111,7 @@ func (uc *usecase) UpdateUser(ctx context.Context, updateData entities.UpdateUse }) } -func (uc *usecase) UpdateUserStatus(ctx context.Context, req entities.UpdateUserStatus) error { +func (uc *usecase) UpdateStatus(ctx context.Context, req entities.UpdateUserStatus) error { _, err := uc.repo.GetUserByID(ctx, req.UserID, entities.LockingOpt{}) if err != nil { uc.log.Z().Err(err).Msg("[usecase]UpdateUserStatus.GetUserByID") @@ -92,3 +127,12 @@ func (uc *usecase) UpdateUserStatus(ctx context.Context, req entities.UpdateUser return nil } + +func (uc *usecase) Detail(ctx context.Context, id int64) (detail dtos.UserDetailResponse, httpCode int, err error) { + userDetail, err := uc.repo.GetUserByID(ctx, id) + if err != nil { + return detail, http.StatusInternalServerError, err + } + + return entities.NewUserDetail(userDetail), http.StatusOK, nil +} diff --git a/internal/users/usecase/usecase_test.go b/internal/users/usecase/usecase_test.go index d24a16f..f791e0f 100644 --- a/internal/users/usecase/usecase_test.go +++ b/internal/users/usecase/usecase_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "net/http" "os" "testing" @@ -12,6 +13,7 @@ import ( "github.com/DoWithLogic/golang-clean-architecture/internal/users/entities" mocks "github.com/DoWithLogic/golang-clean-architecture/internal/users/mock" "github.com/DoWithLogic/golang-clean-architecture/internal/users/usecase" + "github.com/DoWithLogic/golang-clean-architecture/pkg/constant" "github.com/DoWithLogic/golang-clean-architecture/pkg/otel/zerolog" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" @@ -52,32 +54,34 @@ func Test_usecase_CreateUser(t *testing.T) { uc := usecase.NewUseCase( repo, zerolog.NewZeroLog(ctx, os.Stdout), - config.Config{Authentication: config.AuthenticationConfig{Key: "secret-key"}}, + config.Config{Authentication: config.AuthenticationConfig{Key: "DoWithLogic!@#", SecretKey: "s3cr#tK3y!@#"}}, ) - newUser := entities.CreateUser{ + newUser := dtos.CreateUserRequest{ FullName: "fullname", PhoneNumber: "081236548974", - UserType: entities.UserTypePremium, - IsActive: true, + Email: "martinyonatann@testing.com", + Password: "testingPwd", } t.Run("positive_case_create_user", func(t *testing.T) { + repo.EXPECT().IsUserExist(ctx, newUser.Email).Return(false) repo.EXPECT(). SaveNewUser(ctx, createUserMatcher( entities.Users{ - Fullname: "fullname", - PhoneNumber: "081236548974", - UserType: entities.UserTypeRegular, + Fullname: newUser.FullName, + PhoneNumber: newUser.PhoneNumber, + UserType: constant.UserTypeRegular, IsActive: true, }, )). Return(int64(1), nil) - userID, err := uc.CreateUser(ctx, newUser) + userID, httpCode, err := uc.Create(ctx, newUser) require.NoError(t, err) + require.Equal(t, httpCode, http.StatusOK) require.NotNil(t, userID) }) @@ -88,15 +92,16 @@ func Test_usecase_CreateUser(t *testing.T) { entities.Users{ Fullname: "fullname", PhoneNumber: "081236548974", - UserType: entities.UserTypeRegular, + UserType: constant.UserTypeRegular, IsActive: true, }, )). Return(int64(0), errors.New("something errors")) - createdData, err := uc.CreateUser(ctx, newUser) + userID, httpCode, err := uc.Create(ctx, newUser) require.Error(t, err) - require.Equal(t, createdData, dtos.CreateUserResponse(dtos.CreateUserResponse{UserID: 0, Token: "", ExpiredAt: 0})) + require.Equal(t, httpCode, http.StatusInternalServerError) + require.Equal(t, userID, 0) }) } @@ -126,7 +131,7 @@ func Test_usecase_UpdateUserStatus(t *testing.T) { UpdateUserStatusByID(ctx, gomock.Any()). Return(nil) - err := uc.UpdateUserStatus(ctx, args) + err := uc.UpdateStatus(ctx, args) require.NoError(t, err) }) @@ -135,7 +140,7 @@ func Test_usecase_UpdateUserStatus(t *testing.T) { GetUserByID(ctx, args.UserID, gomock.Any()). Return(entities.Users{}, errors.New("something errors")) - err := uc.UpdateUserStatus(ctx, args) + err := uc.UpdateStatus(ctx, args) require.Error(t, err) }) @@ -148,7 +153,7 @@ func Test_usecase_UpdateUserStatus(t *testing.T) { UpdateUserStatusByID(ctx, gomock.Any()). Return(errors.New("there was error")) - err := uc.UpdateUserStatus(ctx, args) + err := uc.UpdateStatus(ctx, args) require.Error(t, err) }) diff --git a/pkg/apperror/errors.go b/pkg/apperror/errors.go new file mode 100644 index 0000000..01ed266 --- /dev/null +++ b/pkg/apperror/errors.go @@ -0,0 +1,10 @@ +package apperror + +import "errors" + +var ( + ErrEmailAlreadyExist = errors.New("email already exist") + ErrInvalidUserType = errors.New("invalid user_type") + ErrInvalidPassword = errors.New("invalid password") + ErrFailedGenerateJWT = errors.New("failed generate access token") +) diff --git a/pkg/constant/constant.go b/pkg/constant/constant.go new file mode 100644 index 0000000..b770313 --- /dev/null +++ b/pkg/constant/constant.go @@ -0,0 +1,19 @@ +package constant + +import "errors" + +const ( + UserTypeRegular = "regular_user" + UserTypePremium = "premium_user" + + UserSystem = "SYSTEM" +) + +var ( + ErrInvalidLockOpt = errors.New("can't do lock with multiple type") +) + +const ( + LengthOfSalt = 16 + LengthOfRandomKey = 32 +) diff --git a/pkg/middleware/auth.go b/pkg/middleware/auth.go index 98a6fe7..185db26 100644 --- a/pkg/middleware/auth.go +++ b/pkg/middleware/auth.go @@ -8,27 +8,20 @@ import ( "github.com/dgrijalva/jwt-go" "github.com/DoWithLogic/golang-clean-architecture/config" - "github.com/DoWithLogic/golang-clean-architecture/internal/users/dtos" + "github.com/DoWithLogic/golang-clean-architecture/pkg/utils/response" "github.com/labstack/echo/v4" ) // CustomClaims represents the custom claims you want to include in the JWT payload. type CustomClaims struct { - UserID int64 `json:"user_id"` + UserID int64 `json:"user_id"` + Email string `json:"email"` jwt.StandardClaims } -func GenerateJWT(userID int64, expiredAt int64, secretKey string) (string, error) { - // Create custom claims - claims := CustomClaims{ - UserID: userID, - StandardClaims: jwt.StandardClaims{ - ExpiresAt: expiredAt, - }, - } - +func GenerateJWT(data CustomClaims, secretKey string) (string, error) { // Create the token - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + token := jwt.NewWithClaims(jwt.SigningMethodHS256, data) // Sign the token with the secret key tokenString, err := token.SignedString([]byte(secretKey)) @@ -44,7 +37,7 @@ func AuthorizeJWT(cfg config.Config) echo.MiddlewareFunc { return func(c echo.Context) error { auth, err := extractBearerToken(c) if err != nil { - return c.JSON(http.StatusUnauthorized, dtos.NewResponseError(http.StatusUnauthorized, dtos.MsgFailed, err.Error())) + return c.JSON(http.StatusUnauthorized, response.NewResponseError(http.StatusUnauthorized, response.MsgFailed, err.Error())) } token, err := jwt.ParseWithClaims(*auth, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) { @@ -52,7 +45,7 @@ func AuthorizeJWT(cfg config.Config) echo.MiddlewareFunc { }) if err != nil { - return c.JSON(http.StatusUnauthorized, dtos.NewResponseError(http.StatusUnauthorized, dtos.MsgFailed, err.Error())) + return c.JSON(http.StatusUnauthorized, response.NewResponseError(http.StatusUnauthorized, response.MsgFailed, err.Error())) } if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid { @@ -61,7 +54,7 @@ func AuthorizeJWT(cfg config.Config) echo.MiddlewareFunc { return next(c) } - return c.JSON(http.StatusUnauthorized, dtos.NewResponseError(http.StatusUnauthorized, dtos.MsgFailed, err.Error())) + return c.JSON(http.StatusUnauthorized, response.NewResponseError(http.StatusUnauthorized, response.MsgFailed, err.Error())) } } } diff --git a/pkg/utils/crypto.go b/pkg/utils/crypto.go new file mode 100644 index 0000000..d55ab27 --- /dev/null +++ b/pkg/utils/crypto.go @@ -0,0 +1,79 @@ +package utils + +import ( + "crypto/aes" + "crypto/cipher" + "encoding/base64" + + "github.com/DoWithLogic/golang-clean-architecture/config" +) + +func Encrypt(pwd string, cfg config.Config) string { + return encrypt(pwd, []byte(cfg.Authentication.SecretKey), []byte(cfg.Authentication.SaltKey)) +} + +func Decrypt(pwd string, cfg config.Config) string { + return decrypt(pwd, []byte(cfg.Authentication.SecretKey), []byte(cfg.Authentication.SaltKey)) +} + +func encrypt(text string, key, salt []byte) string { + plaintext := []byte(text) + + // Create a new AES cipher block + block, err := aes.NewCipher(key) + if err != nil { + panic(err.Error()) + } + + // Create a GCM (Galois/Counter Mode) cipher using AES + gcm, err := cipher.NewGCM(block) + if err != nil { + panic(err.Error()) + } + + // Create a nonce by concatenating salt and random bytes. Nonce must be unique for each encryption + nonce := make([]byte, gcm.NonceSize()) + copy(nonce, salt) + + // Encrypt the data using AES-GCM + ciphertext := gcm.Seal(nil, nonce, plaintext, nil) + + // Include the nonce in the encrypted data + encryptedData := append(nonce, ciphertext...) + + return base64.StdEncoding.EncodeToString(encryptedData) +} + +func decrypt(encryptedText string, key, salt []byte) string { + // Decode base64 + encryptedData, err := base64.StdEncoding.DecodeString(encryptedText) + if err != nil { + panic(err.Error()) + } + + // Create a new AES cipher block + block, err := aes.NewCipher(key) + if err != nil { + panic(err.Error()) + } + + // Create a GCM (Galois/Counter Mode) cipher using AES + gcm, err := cipher.NewGCM(block) + if err != nil { + panic(err.Error()) + } + + // Nonce size is determined by the choice of GCM mode and its associated size for the given key + nonceSize := gcm.NonceSize() + + // Extract the nonce from the encrypted data + nonce, encryptedMessage := encryptedData[:nonceSize], encryptedData[nonceSize:] + + // Decrypt the data using AES-GCM + plaintext, err := gcm.Open(nil, nonce, encryptedMessage, nil) + if err != nil { + panic(err.Error()) + } + + return string(plaintext) +} diff --git a/internal/users/dtos/response.go b/pkg/utils/response/response.go similarity index 98% rename from internal/users/dtos/response.go rename to pkg/utils/response/response.go index 4ae2b63..0142dd0 100644 --- a/internal/users/dtos/response.go +++ b/pkg/utils/response/response.go @@ -1,4 +1,4 @@ -package dtos +package response import ( "net/http"