From 298b0de7254bd21203f2392f8737ceaac5c52f12 Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Fri, 21 Mar 2025 23:34:53 +0700 Subject: [PATCH] update: completely register with otp request --- internal/handler/auth_handler.go | 30 +++-- internal/repositories/auth_repo.go | 81 +------------- internal/services/auth_service.go | 172 +++++++++++++++-------------- presentation/auth_route.go | 37 +------ utils/redis_caching.go | 53 ++++++--- 5 files changed, 147 insertions(+), 226 deletions(-) diff --git a/internal/handler/auth_handler.go b/internal/handler/auth_handler.go index 4653495..896df83 100644 --- a/internal/handler/auth_handler.go +++ b/internal/handler/auth_handler.go @@ -13,41 +13,37 @@ type AuthHandler struct { } func NewAuthHandler(authService services.AuthService) *AuthHandler { - return &AuthHandler{ - authService: authService, - } + return &AuthHandler{authService} } -func (h *AuthHandler) Register(c *fiber.Ctx) error { - var request dto.RegisterRequest - - if err := c.BodyParser(&request); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{"body": {"invalid request body"}}) +func (h *AuthHandler) RegisterUser(c *fiber.Ctx) error { + var req dto.RegisterRequest + if err := c.BodyParser(&req); err != nil { + return utils.ErrorResponse(c, "Invalid request body") } - if errors, valid := request.Validate(); !valid { + if errors, valid := req.Validate(); !valid { return utils.ValidationErrorResponse(c, errors) } - err := h.authService.RegisterUser(&request) + err := h.authService.RegisterUser(&req) if err != nil { return utils.ErrorResponse(c, err.Error()) } - return utils.SuccessResponse(c, nil, "OTP has been sent to your phone") + return utils.SuccessResponse(c, nil, "Kode OTP telah dikirimkan ke nomor WhatsApp anda") } func (h *AuthHandler) VerifyOTP(c *fiber.Ctx) error { - var request dto.VerifyOTPRequest - - if err := c.BodyParser(&request); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{"body": {"invalid request body"}}) + var req dto.VerifyOTPRequest + if err := c.BodyParser(&req); err != nil { + return utils.ErrorResponse(c, "Invalid request body") } - err := h.authService.VerifyOTP(&request) + response, err := h.authService.VerifyOTP(&req) if err != nil { return utils.ErrorResponse(c, err.Error()) } - return utils.SuccessResponse(c, nil, "User successfully registered") + return utils.SuccessResponse(c, response, "Registration successful") } diff --git a/internal/repositories/auth_repo.go b/internal/repositories/auth_repo.go index 0869d2b..24ed934 100644 --- a/internal/repositories/auth_repo.go +++ b/internal/repositories/auth_repo.go @@ -1,20 +1,14 @@ package repositories import ( - "context" - "encoding/json" - "fmt" - "time" - "rijig/model" - "github.com/go-redis/redis/v8" "gorm.io/gorm" ) type UserRepository interface { - SaveUser(user *model.User) (*model.User, error) - FindByPhone(phone string) (*model.User, error) + CreateUser(user *model.User) (*model.User, error) + GetUserByPhone(phone string) (*model.User, error) } type userRepository struct { @@ -22,83 +16,20 @@ type userRepository struct { } func NewUserRepository(db *gorm.DB) UserRepository { - return &userRepository{db: db} + return &userRepository{db} } -func (r *userRepository) SaveUser(user *model.User) (*model.User, error) { +func (r *userRepository) CreateUser(user *model.User) (*model.User, error) { if err := r.db.Create(user).Error; err != nil { return nil, err } return user, nil } -func (r *userRepository) FindByPhone(phone string) (*model.User, error) { +func (r *userRepository) GetUserByPhone(phone string) (*model.User, error) { var user model.User if err := r.db.Where("phone = ?", phone).First(&user).Error; err != nil { - if err == gorm.ErrRecordNotFound { - return nil, nil - } return nil, err } return &user, nil -} - -type RedisRepository interface { - StoreData(key string, data interface{}, expiration time.Duration) error - GetData(key string) (interface{}, error) // Mengembalikan interface{} - DeleteData(key string) error -} - -type redisRepository struct { - client *redis.Client - ctx context.Context -} - -// NewRedisRepository membuat instance baru dari redisRepository -func NewRedisRepository(client *redis.Client) RedisRepository { - return &redisRepository{ - client: client, - ctx: context.Background(), - } -} - -// StoreData menyimpan data ke dalam Redis (dalam format JSON) -func (r *redisRepository) StoreData(key string, data interface{}, expiration time.Duration) error { - // Marshaling data ke JSON - jsonData, err := json.Marshal(data) - if err != nil { - return fmt.Errorf("failed to marshal data: %v", err) - } - - // Simpan JSON ke Redis - err = r.client.Set(r.ctx, key, jsonData, expiration).Err() - if err != nil { - return fmt.Errorf("failed to store data in Redis: %v", err) - } - return nil -} - -// GetData mengambil data dari Redis berdasarkan key -func (r *redisRepository) GetData(key string) (interface{}, error) { - val, err := r.client.Get(r.ctx, key).Result() - if err != nil { - return nil, fmt.Errorf("failed to get data from Redis: %v", err) - } - - // Unmarshal data JSON kembali ke objek - var data interface{} - err = json.Unmarshal([]byte(val), &data) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal data: %v", err) - } - return data, nil -} - -// DeleteData menghapus data di Redis berdasarkan key -func (r *redisRepository) DeleteData(key string) error { - err := r.client.Del(r.ctx, key).Err() - if err != nil { - return fmt.Errorf("failed to delete data from Redis: %v", err) - } - return nil -} +} \ No newline at end of file diff --git a/internal/services/auth_service.go b/internal/services/auth_service.go index 0c05b6e..80d4c7f 100644 --- a/internal/services/auth_service.go +++ b/internal/services/auth_service.go @@ -1,140 +1,142 @@ package services import ( - "encoding/json" + "errors" "fmt" - "log" - "time" - "math/rand" - "rijig/config" "rijig/dto" "rijig/internal/repositories" "rijig/model" + "rijig/utils" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" ) type AuthService interface { - RegisterUser(request *dto.RegisterRequest) error - VerifyOTP(request *dto.VerifyOTPRequest) error + RegisterUser(req *dto.RegisterRequest) error + VerifyOTP(req *dto.VerifyOTPRequest) (*dto.UserDataResponse, error) } type authService struct { - userRepo repositories.UserRepository - roleRepo repositories.RoleRepository - redisRepo repositories.RedisRepository + userRepo repositories.UserRepository + roleRepo repositories.RoleRepository } -func NewAuthService(userRepo repositories.UserRepository, roleRepo repositories.RoleRepository, redisRepo repositories.RedisRepository) AuthService { - return &authService{ - userRepo: userRepo, - roleRepo: roleRepo, - redisRepo: redisRepo, - } +func NewAuthService(userRepo repositories.UserRepository, roleRepo repositories.RoleRepository) AuthService { + return &authService{userRepo, roleRepo} } -func (s *authService) RegisterUser(request *dto.RegisterRequest) error { +func (s *authService) RegisterUser(req *dto.RegisterRequest) error { - if request.RoleID == "" { - return fmt.Errorf("role_id cannot be empty") + userID := uuid.New().String() + + user := &model.User{ + Phone: req.Phone, + RoleID: req.RoleID, } - role, err := s.roleRepo.FindByID(request.RoleID) + err := utils.SetJSONData("user:"+userID, user, 10*time.Minute) if err != nil { - return fmt.Errorf("role not found: %v", err) - } - if role == nil { - return fmt.Errorf("role with ID %s not found", request.RoleID) + return err } - existingUser, err := s.userRepo.FindByPhone(request.Phone) + err = utils.SetStringData("user_phone:"+req.Phone, userID, 10*time.Minute) if err != nil { - return fmt.Errorf("failed to check existing user: %v", err) - } - if existingUser != nil { - return fmt.Errorf("phone number already registered") - } - - temporaryData := &model.User{ - Phone: request.Phone, - RoleID: request.RoleID, - } - - err = s.redisRepo.StoreData(request.Phone, temporaryData, 1*time.Hour) - if err != nil { - return fmt.Errorf("failed to store registration data in Redis: %v", err) + return err } otp := generateOTP() - err = s.redisRepo.StoreData("otp:"+request.Phone, otp, 10*time.Minute) + + err = config.SendWhatsAppMessage(req.Phone, fmt.Sprintf("Your OTP is: %s", otp)) if err != nil { - return fmt.Errorf("failed to store OTP in Redis: %v", err) + return err } - err = config.SendWhatsAppMessage(request.Phone, fmt.Sprintf("Your OTP is: %s", otp)) + err = utils.SetStringData("otp:"+req.Phone, otp, 10*time.Minute) if err != nil { - return fmt.Errorf("failed to send OTP via WhatsApp: %v", err) + return err } - log.Printf("OTP sent to phone number: %s", request.Phone) return nil } -func (s *authService) VerifyOTP(request *dto.VerifyOTPRequest) error { - - storedOTP, err := s.redisRepo.GetData("otp:" + request.Phone) +func (s *authService) VerifyOTP(req *dto.VerifyOTPRequest) (*dto.UserDataResponse, error) { + storedOTP, err := utils.GetStringData("otp:" + req.Phone) if err != nil { - return fmt.Errorf("failed to retrieve OTP from Redis: %v", err) - } - if storedOTP != request.OTP { - return fmt.Errorf("invalid OTP") + return nil, err } - temporaryData, err := s.redisRepo.GetData(request.Phone) + if storedOTP == "" { + return nil, errors.New("OTP expired or not found") + } + + if storedOTP != req.OTP { + return nil, errors.New("invalid OTP") + } + + userID, err := utils.GetStringData("user_phone:" + req.Phone) + if err != nil || userID == "" { + return nil, errors.New("user data not found in Redis") + } + + userData, err := utils.GetJSONData("user:" + userID) + if err != nil || userData == nil { + return nil, errors.New("user data not found in Redis") + } + + user := &model.User{ + Phone: userData["phone"].(string), + RoleID: userData["roleId"].(string), + } + + createdUser, err := s.userRepo.CreateUser(user) if err != nil { - return fmt.Errorf("failed to get registration data from Redis: %v", err) - } - if temporaryData == "" { - return fmt.Errorf("no registration data found for phone: %s", request.Phone) + return nil, err } - temporaryDataStr, ok := temporaryData.(string) - if !ok { - return fmt.Errorf("failed to assert data to string") - } - - temporaryDataBytes := []byte(temporaryDataStr) - - var user model.User - err = json.Unmarshal(temporaryDataBytes, &user) + role, err := s.roleRepo.FindByID(createdUser.RoleID) if err != nil { - return fmt.Errorf("failed to unmarshal registration data: %v", err) + return nil, err } - _, err = s.userRepo.SaveUser(&user) + token, err := generateJWTToken(createdUser.ID) if err != nil { - return fmt.Errorf("failed to save user to database: %v", err) + return nil, err } - err = s.redisRepo.DeleteData(request.Phone) - if err != nil { - return fmt.Errorf("failed to delete registration data from Redis: %v", err) - } - - err = s.redisRepo.DeleteData("otp:" + request.Phone) - if err != nil { - return fmt.Errorf("failed to delete OTP from Redis: %v", err) - } - - return nil + return &dto.UserDataResponse{ + UserID: createdUser.ID, + UserRole: role.RoleName, + Token: token, + }, nil } func generateOTP() string { - - return fmt.Sprintf("%06d", RandomInt(100000, 999999)) -} - -func RandomInt(min, max int) int { rand.Seed(time.Now().UnixNano()) - return rand.Intn(max-min+1) + min + otp := fmt.Sprintf("%06d", rand.Intn(1000000)) + return otp +} + +func generateJWTToken(userID string) (string, error) { + + expirationTime := time.Now().Add(24 * time.Hour) + + claims := &jwt.RegisteredClaims{ + Issuer: userID, + ExpiresAt: jwt.NewNumericDate(expirationTime), + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + secretKey := config.GetSecretKey() + + signedToken, err := token.SignedString([]byte(secretKey)) + if err != nil { + return "", err + } + + return signedToken, nil } diff --git a/presentation/auth_route.go b/presentation/auth_route.go index d0f8f33..4d91242 100644 --- a/presentation/auth_route.go +++ b/presentation/auth_route.go @@ -7,48 +7,15 @@ import ( "rijig/internal/services" "github.com/gofiber/fiber/v2" - // "gorm.io/gorm" - // "rijig/middleware" ) func AuthRouter(api fiber.Router) { - // userRepo := repositories.NewUserRepository(config.DB) - // roleRepo := repositories.NewRoleRepository(config.DB) - // userService := services.NewUserService(userRepo, roleRepo, secretKey) - // userHandler := handler.NewUserHandler(userService) - - // api.Post("/login", userHandler.Login) - // api.Post("/register", userHandler.Register) - // api.Post("/logout", middleware.AuthMiddleware, userHandler.Logout) - // userRepo := repositories.NewUserRepository(config.DB) - // authService := services.NewAuthService(userRepo, secretKey) - - // // Inisialisasi handler - // authHandler := handler.NewAuthHandler(authService) - - // // Endpoint OTP - // authRoutes := api.Group("/auth") - // authRoutes.Post("/send-otp", authHandler.SendOTP) - // authRoutes.Post("/verify-otp", authHandler.VerifyOTP) - // userRepo := repositories.NewUserRepository(config.DB) - // authService := services.NewAuthService(userRepo) - - // authHandler := handler.NewAuthHandler(authService) - - // // Routes - // api.Post("/register", authHandler.Register) - // api.Post("/verify-otp", authHandler.VerifyOTP) userRepo := repositories.NewUserRepository(config.DB) roleRepo := repositories.NewRoleRepository(config.DB) - redisRepo := repositories.NewRedisRepository(config.RedisClient) + authService := services.NewAuthService(userRepo, roleRepo) - // Setup Service - authService := services.NewAuthService(userRepo, roleRepo, redisRepo) - - // Setup Handler authHandler := handler.NewAuthHandler(authService) - // Define Routes - api.Post("/register", authHandler.Register) // Route untuk registrasi + api.Post("/register", authHandler.RegisterUser) api.Post("/verify-otp", authHandler.VerifyOTP) } diff --git a/utils/redis_caching.go b/utils/redis_caching.go index 99d4e8c..7b7bc18 100644 --- a/utils/redis_caching.go +++ b/utils/redis_caching.go @@ -124,18 +124,43 @@ func GetStringData(key string) (string, error) { return val, nil } -func StoreOTPInRedis(phone, otpCode string, expirationTime time.Duration) error { - err := config.RedisClient.Set(config.Ctx, phone, otpCode, expirationTime).Err() - if err != nil { - return fmt.Errorf("failed to store OTP in Redis: %v", err) - } - return nil -} +// func SetStringData(key, value string, expiration time.Duration) error { +// if expiration == 0 { +// expiration = defaultExpiration +// } -func GetOTPFromRedis(phone string) (string, error) { - otpCode, err := config.RedisClient.Get(config.Ctx, phone).Result() - if err != nil { - return "", fmt.Errorf("failed to get OTP from Redis: %v", err) - } - return otpCode, nil -} +// err := config.RedisClient.Set(ctx, key, value, expiration).Err() +// if err != nil { +// return fmt.Errorf("Error setting string data in Redis with key: %s: %v", key, err) +// } + +// log.Printf("String data stored in Redis with key: %s", key) +// return nil +// } + +// GetStringData retrieves a string value from Redis by key +// func GetStringData(key string) (string, error) { +// val, err := config.RedisClient.Get(ctx, key).Result() +// if err == redis.Nil { +// return "", nil +// } else if err != nil { +// return "", fmt.Errorf("Error retrieving string data from Redis with key: %s: %v", key, err) +// } + +// return val, nil +// } +// func StoreOTPInRedis(phone, otpCode string, expirationTime time.Duration) error { +// err := config.RedisClient.Set(config.Ctx, phone, otpCode, expirationTime).Err() +// if err != nil { +// return fmt.Errorf("failed to store OTP in Redis: %v", err) +// } +// return nil +// } + +// func GetOTPFromRedis(phone string) (string, error) { +// otpCode, err := config.RedisClient.Get(config.Ctx, phone).Result() +// if err != nil { +// return "", fmt.Errorf("failed to get OTP from Redis: %v", err) +// } +// return otpCode, nil +// }