From bb85fe64d71e190f1888283bf88809600f5d8ea3 Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Wed, 5 Feb 2025 01:06:35 +0700 Subject: [PATCH] feat: add feature user pin verif and create --- config/database.go | 1 + dto/userpin_dto.go | 43 ++++++++++ internal/handler/userpin_handler.go | 79 +++++++++++++++++ internal/repositories/userpin_repo.go | 46 ++++++++++ internal/services/userpin_service.go | 117 ++++++++++++++++++++++++++ model/userpin_model.go | 11 +++ presentation/userpin_route.go | 24 ++++++ router/setup_routes.go.go | 1 + 8 files changed, 322 insertions(+) create mode 100644 dto/userpin_dto.go create mode 100644 internal/handler/userpin_handler.go create mode 100644 internal/repositories/userpin_repo.go create mode 100644 internal/services/userpin_service.go create mode 100644 model/userpin_model.go create mode 100644 presentation/userpin_route.go diff --git a/config/database.go b/config/database.go index 094fbb7..9fa3fdc 100644 --- a/config/database.go +++ b/config/database.go @@ -33,6 +33,7 @@ func ConnectDatabase() { err = DB.AutoMigrate( &model.User{}, &model.Role{}, + &model.UserPin{}, ) if err != nil { log.Fatalf("Error performing auto-migration: %v", err) diff --git a/dto/userpin_dto.go b/dto/userpin_dto.go new file mode 100644 index 0000000..97c176b --- /dev/null +++ b/dto/userpin_dto.go @@ -0,0 +1,43 @@ +package dto + +import ( + "regexp" + "strings" +) + +type UserPinResponseDTO struct { + ID string `json:"id"` + UserID string `json:"userId"` + Pin string `json:"userpin"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} + +type RequestUserPinDTO struct { + Pin string `json:"userpin"` +} + +func (r *RequestUserPinDTO) Validate() (map[string][]string, bool) { + errors := make(map[string][]string) + + if strings.TrimSpace(r.Pin) == "" { + errors["pin"] = append(errors["pin"], "Pin is required") + } + + if len(r.Pin) != 6 { + errors["pin"] = append(errors["pin"], "Pin harus terdiri dari 6 digit") + } else if !isNumeric(r.Pin) { + errors["pin"] = append(errors["pin"], "Pin harus berupa angka") + } + + if len(errors) > 0 { + return errors, false + } + + return nil, true +} + +func isNumeric(s string) bool { + re := regexp.MustCompile(`^[0-9]+$`) + return re.MatchString(s) +} diff --git a/internal/handler/userpin_handler.go b/internal/handler/userpin_handler.go new file mode 100644 index 0000000..1d8b39a --- /dev/null +++ b/internal/handler/userpin_handler.go @@ -0,0 +1,79 @@ +package handler + +import ( + "github.com/gofiber/fiber/v2" + "github.com/pahmiudahgede/senggoldong/dto" + "github.com/pahmiudahgede/senggoldong/internal/services" + "github.com/pahmiudahgede/senggoldong/utils" +) + +type UserPinHandler struct { + UserPinService services.UserPinService +} + +func NewUserPinHandler(userPinService services.UserPinService) *UserPinHandler { + return &UserPinHandler{UserPinService: userPinService} +} + +func (h *UserPinHandler) VerifyUserPin(c *fiber.Ctx) error { + var requestUserPinDTO dto.RequestUserPinDTO + if err := c.BodyParser(&requestUserPinDTO); err != nil { + return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}}) + } + + errors, valid := requestUserPinDTO.Validate() + if !valid { + return utils.ValidationErrorResponse(c, errors) + } + + userID, ok := c.Locals("userID").(string) + if !ok || userID == "" { + return utils.GenericErrorResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") + } + + _, err := h.UserPinService.VerifyUserPin(requestUserPinDTO.Pin, userID) + if err != nil { + return utils.GenericErrorResponse(c, fiber.StatusUnauthorized, "pin yang anda masukkan salah") + } + + return utils.LogResponse(c, map[string]string{"data": "pin yang anda masukkan benar"}, "Pin verification successful") +} + +func (h *UserPinHandler) CheckPinStatus(c *fiber.Ctx) error { + userID, ok := c.Locals("userID").(string) + if !ok || userID == "" { + return utils.GenericErrorResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") + } + + status, _, err := h.UserPinService.CheckPinStatus(userID) + if err != nil { + return utils.GenericErrorResponse(c, fiber.StatusInternalServerError, err.Error()) + } + + if status == "Pin not created" { + return utils.GenericErrorResponse(c, fiber.StatusBadRequest, "pin belum dibuat") + } + + return utils.LogResponse(c, map[string]string{"data": "pin sudah dibuat"}, "Pin status retrieved successfully") +} + +func (h *UserPinHandler) CreateUserPin(c *fiber.Ctx) error { + var requestUserPinDTO dto.RequestUserPinDTO + if err := c.BodyParser(&requestUserPinDTO); err != nil { + return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}}) + } + + errors, valid := requestUserPinDTO.Validate() + if !valid { + return utils.ValidationErrorResponse(c, errors) + } + + userID := c.Locals("userID").(string) + + userPinResponse, err := h.UserPinService.CreateUserPin(userID, requestUserPinDTO.Pin) + if err != nil { + return utils.GenericErrorResponse(c, fiber.StatusConflict, err.Error()) + } + + return utils.LogResponse(c, userPinResponse, "User pin created successfully") +} diff --git a/internal/repositories/userpin_repo.go b/internal/repositories/userpin_repo.go new file mode 100644 index 0000000..d6bf2d3 --- /dev/null +++ b/internal/repositories/userpin_repo.go @@ -0,0 +1,46 @@ +package repositories + +import ( + "github.com/pahmiudahgede/senggoldong/model" + "gorm.io/gorm" +) + +type UserPinRepository interface { + FindByUserID(userID string) (*model.UserPin, error) + FindByPin(userPin string) (*model.UserPin, error) + Create(userPin *model.UserPin) error +} + +type userPinRepository struct { + DB *gorm.DB +} + +func NewUserPinRepository(db *gorm.DB) UserPinRepository { + return &userPinRepository{DB: db} +} + +func (r *userPinRepository) FindByUserID(userID string) (*model.UserPin, error) { + var userPin model.UserPin + err := r.DB.Where("user_id = ?", userID).First(&userPin).Error + if err != nil { + return nil, err + } + return &userPin, nil +} + +func (r *userPinRepository) FindByPin(pin string) (*model.UserPin, error) { + var userPin model.UserPin + err := r.DB.Where("pin = ?", pin).First(&userPin).Error + if err != nil { + return nil, err + } + return &userPin, nil +} + +func (r *userPinRepository) Create(userPin *model.UserPin) error { + err := r.DB.Create(userPin).Error + if err != nil { + return err + } + return nil +} diff --git a/internal/services/userpin_service.go b/internal/services/userpin_service.go new file mode 100644 index 0000000..3cbc873 --- /dev/null +++ b/internal/services/userpin_service.go @@ -0,0 +1,117 @@ +package services + +import ( + "fmt" + "time" + + "github.com/pahmiudahgede/senggoldong/dto" + "github.com/pahmiudahgede/senggoldong/internal/repositories" + "github.com/pahmiudahgede/senggoldong/model" + "github.com/pahmiudahgede/senggoldong/utils" + "golang.org/x/crypto/bcrypt" +) + +type UserPinService interface { + CreateUserPin(userID, pin string) (*dto.UserPinResponseDTO, error) + VerifyUserPin(userID, pin string) (*dto.UserPinResponseDTO, error) + CheckPinStatus(userID string) (string, *dto.UserPinResponseDTO, error) +} + +type userPinService struct { + UserPinRepo repositories.UserPinRepository +} + +func NewUserPinService(userPinRepo repositories.UserPinRepository) UserPinService { + return &userPinService{UserPinRepo: userPinRepo} +} + +func (s *userPinService) VerifyUserPin(pin string, userID string) (*dto.UserPinResponseDTO, error) { + + userPin, err := s.UserPinRepo.FindByUserID(userID) + if err != nil { + return nil, fmt.Errorf("user pin not found") + } + + err = bcrypt.CompareHashAndPassword([]byte(userPin.Pin), []byte(pin)) + if err != nil { + return nil, fmt.Errorf("incorrect pin") + } + + createdAt, _ := utils.FormatDateToIndonesianFormat(userPin.CreatedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(userPin.UpdatedAt) + + userPinResponse := &dto.UserPinResponseDTO{ + ID: userPin.ID, + UserID: userPin.UserID, + Pin: userPin.Pin, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + } + + return userPinResponse, nil +} + +func (s *userPinService) CheckPinStatus(userID string) (string, *dto.UserPinResponseDTO, error) { + userPin, err := s.UserPinRepo.FindByUserID(userID) + if err != nil { + return "Pin not created", nil, nil + } + + createdAt, _ := utils.FormatDateToIndonesianFormat(userPin.CreatedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(userPin.UpdatedAt) + + userPinResponse := &dto.UserPinResponseDTO{ + ID: userPin.ID, + UserID: userPin.UserID, + Pin: userPin.Pin, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + } + + return "Pin already created", userPinResponse, nil +} + +func (s *userPinService) CreateUserPin(userID, pin string) (*dto.UserPinResponseDTO, error) { + + existingPin, err := s.UserPinRepo.FindByUserID(userID) + if err != nil && existingPin != nil { + return nil, fmt.Errorf("you have already created a pin, you don't need to create another one") + } + + hashedPin, err := bcrypt.GenerateFromPassword([]byte(pin), bcrypt.DefaultCost) + if err != nil { + return nil, fmt.Errorf("error hashing the pin: %v", err) + } + + newPin := model.UserPin{ + UserID: userID, + Pin: string(hashedPin), + } + + err = s.UserPinRepo.Create(&newPin) + if err != nil { + return nil, fmt.Errorf("error creating user pin: %v", err) + } + + createdAt, _ := utils.FormatDateToIndonesianFormat(newPin.CreatedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(newPin.UpdatedAt) + + userPinResponse := &dto.UserPinResponseDTO{ + ID: newPin.ID, + UserID: newPin.UserID, + Pin: newPin.Pin, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + } + + cacheKey := fmt.Sprintf("userpin:%s", userID) + cacheData := map[string]interface{}{ + "data": userPinResponse, + } + err = utils.SetJSONData(cacheKey, cacheData, time.Hour*24) + if err != nil { + fmt.Printf("Error caching new user pin to Redis: %v\n", err) + } + + return userPinResponse, nil +} diff --git a/model/userpin_model.go b/model/userpin_model.go new file mode 100644 index 0000000..0ad17be --- /dev/null +++ b/model/userpin_model.go @@ -0,0 +1,11 @@ +package model + +import "time" + +type UserPin struct { + ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` + UserID string `gorm:"not null" json:"userId"` + Pin string `gorm:"not null" json:"pin"` + CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"` + UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"` +} diff --git a/presentation/userpin_route.go b/presentation/userpin_route.go new file mode 100644 index 0000000..335cd43 --- /dev/null +++ b/presentation/userpin_route.go @@ -0,0 +1,24 @@ +package presentation + +import ( + "github.com/gofiber/fiber/v2" + "github.com/pahmiudahgede/senggoldong/config" + "github.com/pahmiudahgede/senggoldong/internal/handler" + "github.com/pahmiudahgede/senggoldong/internal/repositories" + "github.com/pahmiudahgede/senggoldong/internal/services" + "github.com/pahmiudahgede/senggoldong/middleware" +) + +func UserPinRouter(api fiber.Router) { + + userPinRepo := repositories.NewUserPinRepository(config.DB) + + userPinService := services.NewUserPinService(userPinRepo) + + userPinHandler := handler.NewUserPinHandler(userPinService) + + api.Post("/user/set-pin", middleware.AuthMiddleware, userPinHandler.CreateUserPin) + api.Post("/user/verif-pin", middleware.AuthMiddleware, userPinHandler.VerifyUserPin) + api.Get("/user/cek-pin-status", middleware.AuthMiddleware, userPinHandler.CheckPinStatus) + +} diff --git a/router/setup_routes.go.go b/router/setup_routes.go.go index 12e5c27..c538fde 100644 --- a/router/setup_routes.go.go +++ b/router/setup_routes.go.go @@ -11,4 +11,5 @@ func SetupRoutes(app *fiber.App) { api.Use(middleware.APIKeyMiddleware) presentation.AuthRouter(api) presentation.UserProfileRouter(api) + presentation.UserPinRouter(api) }