feat: add feature user pin verif and create

This commit is contained in:
pahmiudahgede 2025-02-05 01:06:35 +07:00
parent b91c35333d
commit bb85fe64d7
8 changed files with 322 additions and 0 deletions

View File

@ -33,6 +33,7 @@ func ConnectDatabase() {
err = DB.AutoMigrate( err = DB.AutoMigrate(
&model.User{}, &model.User{},
&model.Role{}, &model.Role{},
&model.UserPin{},
) )
if err != nil { if err != nil {
log.Fatalf("Error performing auto-migration: %v", err) log.Fatalf("Error performing auto-migration: %v", err)

43
dto/userpin_dto.go Normal file
View File

@ -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)
}

View File

@ -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")
}

View File

@ -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
}

View File

@ -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
}

11
model/userpin_model.go Normal file
View File

@ -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"`
}

View File

@ -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)
}

View File

@ -11,4 +11,5 @@ func SetupRoutes(app *fiber.App) {
api.Use(middleware.APIKeyMiddleware) api.Use(middleware.APIKeyMiddleware)
presentation.AuthRouter(api) presentation.AuthRouter(api)
presentation.UserProfileRouter(api) presentation.UserProfileRouter(api)
presentation.UserPinRouter(api)
} }