diff --git a/config/connection.go b/config/connection.go index 452b35d..3fe6ed8 100644 --- a/config/connection.go +++ b/config/connection.go @@ -53,6 +53,9 @@ func InitDatabase() { err = DB.AutoMigrate( &domain.User{}, &domain.UserRole{}, + &domain.UserPin{}, + &domain.MenuAccess{}, + &domain.PlatformHandle{}, &domain.Address{}, ) if err != nil { diff --git a/domain/address.go b/domain/address.go index b47b4ed..9d6075a 100644 --- a/domain/address.go +++ b/domain/address.go @@ -1,8 +1,6 @@ package domain -import ( - "time" -) +import "time" type Address struct { ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` diff --git a/domain/menuaccess.go b/domain/menuaccess.go new file mode 100644 index 0000000..b5d2f42 --- /dev/null +++ b/domain/menuaccess.go @@ -0,0 +1,14 @@ +package domain + +import "time" + +type MenuAccess struct { + ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` + RoleID string `gorm:"not null" json:"roleId"` + Role UserRole `gorm:"foreignKey:RoleID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"role"` + MenuName string `gorm:"not null" json:"menuName"` + Path string `gorm:"not null" json:"path"` + IconURL string `gorm:"not null" json:"iconUrl"` + CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"` + UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"` +} diff --git a/domain/platform.go b/domain/platform.go new file mode 100644 index 0000000..b75b8e7 --- /dev/null +++ b/domain/platform.go @@ -0,0 +1,7 @@ +package domain + +type PlatformHandle struct { + ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` + Platform string `gorm:"not null" json:"platform"` + Description string `gorm:"not null" json:"description"` +} diff --git a/domain/user.go b/domain/user.go index 80582e4..73d1bf2 100644 --- a/domain/user.go +++ b/domain/user.go @@ -1,18 +1,17 @@ package domain -import ( - "time" -) +import "time" type User struct { ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` Avatar *string `json:"avatar,omitempty"` Username string `gorm:"unique;not null" json:"username"` - Name string `gorm:"not null" json:"name"` + Name string `gorm:"not null" json:"name"` Phone string `gorm:"not null" json:"phone"` Email string `gorm:"unique;not null" json:"email"` EmailVerified bool `gorm:"default:false" json:"emailVerified"` Password string `gorm:"not null" json:"password"` + Pin UserPin `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"pin"` CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"` UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"` RoleID string `gorm:"not null" json:"roleId"` diff --git a/domain/userpin.go b/domain/userpin.go new file mode 100644 index 0000000..a8c5d4c --- /dev/null +++ b/domain/userpin.go @@ -0,0 +1,11 @@ +package domain + +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/dto/address.go b/dto/address.go index 4740552..d74696d 100644 --- a/dto/address.go +++ b/dto/address.go @@ -16,8 +16,6 @@ type AddressInput struct { Geography string `json:"geography" validate:"required"` } -var validate = validator.New() - func (c *AddressInput) ValidatePost() error { err := validate.Struct(c) if err != nil { diff --git a/dto/userpin.go b/dto/userpin.go new file mode 100644 index 0000000..772eddb --- /dev/null +++ b/dto/userpin.go @@ -0,0 +1,44 @@ +package dto + +import ( + "fmt" + + "github.com/go-playground/validator/v10" +) + +type PinInput struct { + Pin string `json:"pin" validate:"required,len=6,numeric"` +} + +func (p *PinInput) ValidateCreate() error { + err := validate.Struct(p) + if err != nil { + for _, e := range err.(validator.ValidationErrors) { + switch e.Field() { + case "Pin": + return fmt.Errorf("PIN harus terdiri dari 6 digit angka") + } + } + } + return nil +} + +type PinUpdateInput struct { + OldPin string `json:"old_pin" validate:"required,len=6,numeric"` + NewPin string `json:"new_pin" validate:"required,len=6,numeric"` +} + +func (p *PinUpdateInput) ValidateUpdate() error { + err := validate.Struct(p) + if err != nil { + for _, e := range err.(validator.ValidationErrors) { + switch e.Field() { + case "OldPin": + return fmt.Errorf("PIN lama harus terdiri dari 6 digit angka") + case "NewPin": + return fmt.Errorf("PIN baru harus terdiri dari 6 digit angka") + } + } + } + return nil +} \ No newline at end of file diff --git a/dto/validator.go b/dto/validator.go new file mode 100644 index 0000000..fb5440e --- /dev/null +++ b/dto/validator.go @@ -0,0 +1,5 @@ +package dto + +import "github.com/go-playground/validator/v10" + +var validate = validator.New() \ No newline at end of file diff --git a/internal/api/routes.go b/internal/api/routes.go index c139877..b7a85a6 100644 --- a/internal/api/routes.go +++ b/internal/api/routes.go @@ -7,16 +7,24 @@ import ( ) func AppRouter(app *fiber.App) { + // # authentication app.Post("/register", controllers.Register) app.Post("/login", controllers.Login) + // # userinfo app.Get("/user", middleware.AuthMiddleware, controllers.GetUserInfo) app.Put("/update-user", middleware.AuthMiddleware, controllers.UpdateUser) app.Post("/user/update-password", middleware.AuthMiddleware, controllers.UpdatePassword) + // # user set pin + app.Post("/user/set-pin", middleware.AuthMiddleware, controllers.CreatePin) + app.Get("/user/get-pin", middleware.AuthMiddleware, controllers.GetPin) + app.Put("/user/update-pin", middleware.AuthMiddleware, controllers.UpdatePin) + + // # address routing app.Get("/list-address", middleware.AuthMiddleware, controllers.GetListAddress) app.Get("/address/:id", middleware.AuthMiddleware, controllers.GetAddressByID) app.Post("/create-address", middleware.AuthMiddleware, controllers.CreateAddress) app.Put("/address/:id", middleware.AuthMiddleware, controllers.UpdateAddress) app.Delete("/address/:id", middleware.AuthMiddleware, controllers.DeleteAddress) -} +} \ No newline at end of file diff --git a/internal/controllers/userpin.go b/internal/controllers/userpin.go new file mode 100644 index 0000000..e48c020 --- /dev/null +++ b/internal/controllers/userpin.go @@ -0,0 +1,132 @@ +package controllers + +import ( + "github.com/gofiber/fiber/v2" + "github.com/pahmiudahgede/senggoldong/dto" + "github.com/pahmiudahgede/senggoldong/internal/services" + "github.com/pahmiudahgede/senggoldong/utils" +) + +func CreatePin(c *fiber.Ctx) error { + var input dto.PinInput + if err := c.BodyParser(&input); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(utils.FormatResponse( + fiber.StatusBadRequest, + "Data input tidak valid", + nil, + )) + } + + if err := input.ValidateCreate(); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(utils.FormatResponse( + fiber.StatusBadRequest, + err.Error(), + nil, + )) + } + + userID := c.Locals("userID").(string) + + existingPin, err := services.GetPinByUserID(userID) + if err == nil && existingPin.ID != "" { + return c.Status(fiber.StatusBadRequest).JSON(utils.FormatResponse( + fiber.StatusBadRequest, + "PIN sudah ada, tidak perlu dibuat lagi", + nil, + )) + } + + pin, err := services.CreatePin(userID, input) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(utils.FormatResponse( + fiber.StatusInternalServerError, + "Failed to create PIN", + nil, + )) + } + + pinResponse := map[string]interface{}{ + "id": pin.ID, + "createdAt": pin.CreatedAt, + "updatedAt": pin.UpdatedAt, + } + + return c.Status(fiber.StatusOK).JSON(utils.FormatResponse( + fiber.StatusOK, + "PIN created successfully", + pinResponse, + )) +} + +func GetPin(c *fiber.Ctx) error { + var input dto.PinInput + if err := c.BodyParser(&input); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(utils.FormatResponse( + fiber.StatusBadRequest, + "Data input tidak valid", + nil, + )) + } + + userID := c.Locals("userID").(string) + pin, err := services.GetPinByUserID(userID) + if err != nil { + return c.Status(fiber.StatusNotFound).JSON(utils.FormatResponse( + fiber.StatusNotFound, + "PIN tidak ditemukan", + nil, + )) + } + + isPinValid := services.CheckPin(pin.Pin, input.Pin) + + if isPinValid { + return c.Status(fiber.StatusOK).JSON(utils.FormatResponse( + fiber.StatusOK, + "PIN benar", + true, + )) + } + + return c.Status(fiber.StatusUnauthorized).JSON(utils.FormatResponse( + fiber.StatusUnauthorized, + "PIN salah", + false, + )) +} + +func UpdatePin(c *fiber.Ctx) error { + var input dto.PinUpdateInput + if err := c.BodyParser(&input); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(utils.FormatResponse( + fiber.StatusBadRequest, + "Data input tidak valid", + nil, + )) + } + + if err := input.ValidateUpdate(); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(utils.FormatResponse( + fiber.StatusBadRequest, + err.Error(), + nil, + )) + } + + userID := c.Locals("userID").(string) + + updatedPin, err := services.UpdatePin(userID, input.OldPin, input.NewPin) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(utils.FormatResponse( + fiber.StatusInternalServerError, + "Failed to update PIN", + nil, + )) + } + + return c.Status(fiber.StatusOK).JSON(utils.FormatResponse( + fiber.StatusOK, + "PIN updated successfully", + updatedPin, + )) +} diff --git a/internal/repositories/userpin.go b/internal/repositories/userpin.go new file mode 100644 index 0000000..f48285a --- /dev/null +++ b/internal/repositories/userpin.go @@ -0,0 +1,48 @@ +package repositories + +import ( + "errors" + + "github.com/pahmiudahgede/senggoldong/config" + "github.com/pahmiudahgede/senggoldong/domain" + "golang.org/x/crypto/bcrypt" +) + +func CreatePin(pin *domain.UserPin) error { + result := config.DB.Create(pin) + if result.Error != nil { + return result.Error + } + return nil +} + +func GetPinByUserID(userID string) (domain.UserPin, error) { + var pin domain.UserPin + err := config.DB.Where("user_id = ?", userID).First(&pin).Error + if err != nil { + return pin, errors.New("PIN tidak ditemukan") + } + return pin, nil +} + +func UpdatePin(userID string, newPin string) (domain.UserPin, error) { + var pin domain.UserPin + + err := config.DB.Where("user_id = ?", userID).First(&pin).Error + if err != nil { + return pin, errors.New("PIN tidak ditemukan") + } + + hashedPin, err := bcrypt.GenerateFromPassword([]byte(newPin), bcrypt.DefaultCost) + if err != nil { + return pin, err + } + + pin.Pin = string(hashedPin) + + if err := config.DB.Save(&pin).Error; err != nil { + return pin, err + } + + return pin, nil +} \ No newline at end of file diff --git a/internal/services/userpin.go b/internal/services/userpin.go new file mode 100644 index 0000000..4e9f3f0 --- /dev/null +++ b/internal/services/userpin.go @@ -0,0 +1,62 @@ +package services + +import ( + "errors" + + "github.com/pahmiudahgede/senggoldong/domain" + "github.com/pahmiudahgede/senggoldong/dto" + "github.com/pahmiudahgede/senggoldong/internal/repositories" + "golang.org/x/crypto/bcrypt" +) + +func GetPinByUserID(userID string) (domain.UserPin, error) { + pin, err := repositories.GetPinByUserID(userID) + if err != nil { + return pin, errors.New("PIN tidak ditemukan") + } + return pin, nil +} + +func CreatePin(userID string, input dto.PinInput) (domain.UserPin, error) { + + hashedPin, err := bcrypt.GenerateFromPassword([]byte(input.Pin), bcrypt.DefaultCost) + if err != nil { + return domain.UserPin{}, err + } + + pin := domain.UserPin{ + UserID: userID, + Pin: string(hashedPin), + } + + err = repositories.CreatePin(&pin) + if err != nil { + return domain.UserPin{}, err + } + + return pin, nil +} + +func UpdatePin(userID string, oldPin string, newPin string) (domain.UserPin, error) { + + pin, err := repositories.GetPinByUserID(userID) + if err != nil { + return pin, errors.New("PIN tidak ditemukan") + } + + if err := bcrypt.CompareHashAndPassword([]byte(pin.Pin), []byte(oldPin)); err != nil { + return pin, errors.New("PIN lama tidak cocok") + } + + updatedPin, err := repositories.UpdatePin(userID, newPin) + if err != nil { + return updatedPin, err + } + + return updatedPin, nil +} + +func CheckPin(storedPinHash string, inputPin string) bool { + err := bcrypt.CompareHashAndPassword([]byte(storedPinHash), []byte(inputPin)) + return err == nil +}