From 21d4f28cef9023d7e032d8f7cc994611a0bb8e36 Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Wed, 19 Mar 2025 23:25:19 +0700 Subject: [PATCH] refact: register now can handle otp from phone --- config/server.go | 4 + config/setup_config.go | 1 + config/whatsapp.go | 111 ++++++++++++++ dto/auth_dto.go | 234 ++++++++++++++++++----------- dto/user_dto.go | 10 +- go.mod | 27 +++- go.sum | 60 ++++++-- internal/handler/auth_handler.go | 96 +++++++----- internal/handler/user_handler.go | 38 ++--- internal/repositories/auth_repo.go | 54 ++----- internal/services/auth_service.go | 225 ++++++++++++--------------- internal/services/user_service.go | 76 +++++----- presentation/auth_route.go | 39 +++-- presentation/user_route.go | 2 +- utils/redis_caching.go | 41 +++++ 15 files changed, 617 insertions(+), 401 deletions(-) create mode 100644 config/whatsapp.go diff --git a/config/server.go b/config/server.go index 8fc1592..10caf70 100644 --- a/config/server.go +++ b/config/server.go @@ -8,6 +8,10 @@ import ( "github.com/gofiber/fiber/v2" ) +func GetSecretKey() string { + return os.Getenv("SECRET_KEY") +} + func StartServer(app *fiber.App) { host := os.Getenv("SERVER_HOST") port := os.Getenv("SERVER_PORT") diff --git a/config/setup_config.go b/config/setup_config.go index e6679a2..b302514 100644 --- a/config/setup_config.go +++ b/config/setup_config.go @@ -14,4 +14,5 @@ func SetupConfig() { ConnectDatabase() ConnectRedis() + InitWhatsApp() } diff --git a/config/whatsapp.go b/config/whatsapp.go new file mode 100644 index 0000000..18d073c --- /dev/null +++ b/config/whatsapp.go @@ -0,0 +1,111 @@ +package config + +import ( + "context" + "fmt" + "log" + "os" + "os/signal" + "syscall" + + _ "github.com/lib/pq" + "github.com/mdp/qrterminal/v3" + "go.mau.fi/whatsmeow" + "go.mau.fi/whatsmeow/proto/waE2E" + "go.mau.fi/whatsmeow/store/sqlstore" + "go.mau.fi/whatsmeow/types" + waLog "go.mau.fi/whatsmeow/util/log" + "google.golang.org/protobuf/proto" +) + +var WhatsAppClient *whatsmeow.Client +var container *sqlstore.Container + +func InitWhatsApp() { + dbLog := waLog.Stdout("Database", "DEBUG", true) + + dsn := fmt.Sprintf( + "postgres://%s:%s@%s:%s/%s?sslmode=disable", + os.Getenv("DB_USER"), + os.Getenv("DB_PASSWORD"), + os.Getenv("DB_HOST"), + os.Getenv("DB_PORT"), + os.Getenv("DB_NAME"), + ) + + var err error + container, err = sqlstore.New("postgres", dsn, dbLog) + if err != nil { + log.Fatalf("Failed to connect to WhatsApp database: %v", err) + } + + deviceStore, err := container.GetFirstDevice() + if err != nil { + log.Fatalf("Failed to get WhatsApp device: %v", err) + } + + clientLog := waLog.Stdout("Client", "DEBUG", true) + WhatsAppClient = whatsmeow.NewClient(deviceStore, clientLog) + + if WhatsAppClient.Store.ID == nil { + fmt.Println("WhatsApp Client is not logged in, generating QR Code...") + + qrChan, _ := WhatsAppClient.GetQRChannel(context.Background()) + err = WhatsAppClient.Connect() + if err != nil { + log.Fatalf("Failed to connect WhatsApp client: %v", err) + } + + for evt := range qrChan { + if evt.Event == "code" { + fmt.Println("QR Code untuk login:") + generateQRCode(evt.Code) + } else { + fmt.Println("Login event:", evt.Event) + } + } + } else { + fmt.Println("WhatsApp Client sudah login, langsung terhubung...") + err = WhatsAppClient.Connect() + if err != nil { + log.Fatalf("Failed to connect WhatsApp client: %v", err) + } + } + + log.Println("WhatsApp client connected successfully!") + go handleShutdown() +} + +func generateQRCode(qrString string) { + qrterminal.GenerateHalfBlock(qrString, qrterminal.M, os.Stdout) +} + + +func handleShutdown() { + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) + <-sigChan + + log.Println("Shutting down WhatsApp client...") + WhatsAppClient.Disconnect() + os.Exit(0) +} + +func SendWhatsAppMessage(phone, message string) error { + if WhatsAppClient == nil { + return fmt.Errorf("WhatsApp client is not initialized") + } + + targetJID, _ := types.ParseJID(phone + "@s.whatsapp.net") + msg := waE2E.Message{ + Conversation: proto.String(message), + } + + _, err := WhatsAppClient.SendMessage(context.Background(), targetJID, &msg) + if err != nil { + return fmt.Errorf("failed to send WhatsApp message: %v", err) + } + + log.Printf("WhatsApp message sent successfully to: %s", phone) + return nil +} diff --git a/dto/auth_dto.go b/dto/auth_dto.go index 5a7e2ea..6232f8a 100644 --- a/dto/auth_dto.go +++ b/dto/auth_dto.go @@ -5,39 +5,46 @@ import ( "strings" ) -type LoginDTO struct { - RoleID string `json:"roleid"` - Identifier string `json:"identifier"` - Password string `json:"password"` +type RegisterRequest struct { + RoleID string `json:"role_id"` + Phone string `json:"phone"` } -type UserResponseWithToken struct { +type VerifyOTPRequest struct { + Phone string `json:"phone"` + OTP string `json:"otp"` +} + +type MetaResponse struct { + Status int `json:"status"` + Message string `json:"message"` +} + +// UserDataResponse untuk bagian data +type UserDataResponse struct { UserID string `json:"user_id"` - RoleName string `json:"role_name"` + UserRole string `json:"user_role"` Token string `json:"token"` } -type RegisterDTO struct { - Username string `json:"username"` - Name string `json:"name"` - Phone string `json:"phone"` - Email string `json:"email"` - Password string `json:"password"` - ConfirmPassword string `json:"confirm_password"` - RoleID string `json:"roleId,omitempty"` +// Response struct utama +type Response struct { + Meta MetaResponse `json:"meta"` + Data *UserDataResponse `json:"data,omitempty"` // Gunakan pointer agar bisa bernilai nil jika tidak diperlukan } -func (l *LoginDTO) Validate() (map[string][]string, bool) { +func (l *RegisterRequest) Validate() (map[string][]string, bool) { errors := make(map[string][]string) + // Validasi RoleID dan Phone if strings.TrimSpace(l.RoleID) == "" { errors["roleid"] = append(errors["roleid"], "Role ID is required") } - if strings.TrimSpace(l.Identifier) == "" { - errors["identifier"] = append(errors["identifier"], "Identifier (username, email, or phone) is required") - } - if strings.TrimSpace(l.Password) == "" { - errors["password"] = append(errors["password"], "Password is required") + + if strings.TrimSpace(l.Phone) == "" { + errors["phone"] = append(errors["phone"], "Phone is required") + } else if !IsValidPhoneNumber(l.Phone) { + errors["phone"] = append(errors["phone"], "Invalid phone number format. Use 62 followed by 9-13 digits") } if len(errors) > 0 { @@ -46,76 +53,131 @@ func (l *LoginDTO) Validate() (map[string][]string, bool) { return nil, true } -func (r *RegisterDTO) Validate() (map[string][]string, bool) { - errors := make(map[string][]string) - - r.validateRequiredFields(errors) - - if r.Phone != "" && !IsValidPhoneNumber(r.Phone) { - errors["phone"] = append(errors["phone"], "Invalid phone number format. Use +62 followed by 9-13 digits") - } - - if r.Email != "" && !IsValidEmail(r.Email) { - errors["email"] = append(errors["email"], "Invalid email format") - } - - if r.Password != "" && !IsValidPassword(r.Password) { - errors["password"] = append(errors["password"], "Password must be at least 8 characters long and contain at least one number") - } - - if r.ConfirmPassword != "" && r.Password != r.ConfirmPassword { - errors["confirm_password"] = append(errors["confirm_password"], "Password and confirm password do not match") - } - - if len(errors) > 0 { - return errors, false - } - - return nil, true -} - -func (r *RegisterDTO) validateRequiredFields(errors map[string][]string) { - - if strings.TrimSpace(r.Username) == "" { - errors["username"] = append(errors["username"], "Username is required") - } - if strings.TrimSpace(r.Name) == "" { - errors["name"] = append(errors["name"], "Name is required") - } - if strings.TrimSpace(r.Phone) == "" { - errors["phone"] = append(errors["phone"], "Phone number is required") - } - if strings.TrimSpace(r.Email) == "" { - errors["email"] = append(errors["email"], "Email is required") - } - if strings.TrimSpace(r.Password) == "" { - errors["password"] = append(errors["password"], "Password is required") - } - if strings.TrimSpace(r.ConfirmPassword) == "" { - errors["confirm_password"] = append(errors["confirm_password"], "Confirm password is required") - } - if strings.TrimSpace(r.RoleID) == "" { - errors["roleId"] = append(errors["roleId"], "RoleID is required") - } -} - +// IsValidPhoneNumber untuk validasi format nomor telepon func IsValidPhoneNumber(phone string) bool { - - re := regexp.MustCompile(`^\+62\d{9,13}$`) + // Validasi format nomor telepon harus dimulai dengan 62 dan 9-13 digit setelahnya + re := regexp.MustCompile(`^62\d{9,13}$`) return re.MatchString(phone) } -func IsValidEmail(email string) bool { +// package dto - re := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) - return re.MatchString(email) -} +// import ( +// "regexp" +// "strings" +// ) -func IsValidPassword(password string) bool { - if len(password) < 8 { - return false - } +// type LoginDTO struct { +// RoleID string `json:"roleid"` +// Identifier string `json:"identifier"` +// Password string `json:"password"` +// } - re := regexp.MustCompile(`\d`) - return re.MatchString(password) -} +// type UserResponseWithToken struct { +// UserID string `json:"user_id"` +// RoleName string `json:"role_name"` +// Token string `json:"token"` +// } + +// type RegisterDTO struct { +// Username string `json:"username"` +// Name string `json:"name"` +// Phone string `json:"phone"` +// Email string `json:"email"` +// Password string `json:"password"` +// ConfirmPassword string `json:"confirm_password"` +// RoleID string `json:"roleId,omitempty"` +// } + +// func (l *LoginDTO) Validate() (map[string][]string, bool) { +// errors := make(map[string][]string) + +// if strings.TrimSpace(l.RoleID) == "" { +// errors["roleid"] = append(errors["roleid"], "Role ID is required") +// } +// if strings.TrimSpace(l.Identifier) == "" { +// errors["identifier"] = append(errors["identifier"], "Identifier (username, email, or phone) is required") +// } +// if strings.TrimSpace(l.Password) == "" { +// errors["password"] = append(errors["password"], "Password is required") +// } + +// if len(errors) > 0 { +// return errors, false +// } +// return nil, true +// } + +// func (r *RegisterDTO) Validate() (map[string][]string, bool) { +// errors := make(map[string][]string) + +// r.validateRequiredFields(errors) + +// if r.Phone != "" && !IsValidPhoneNumber(r.Phone) { +// errors["phone"] = append(errors["phone"], "Invalid phone number format. Use +62 followed by 9-13 digits") +// } + +// if r.Email != "" && !IsValidEmail(r.Email) { +// errors["email"] = append(errors["email"], "Invalid email format") +// } + +// if r.Password != "" && !IsValidPassword(r.Password) { +// errors["password"] = append(errors["password"], "Password must be at least 8 characters long and contain at least one number") +// } + +// if r.ConfirmPassword != "" && r.Password != r.ConfirmPassword { +// errors["confirm_password"] = append(errors["confirm_password"], "Password and confirm password do not match") +// } + +// if len(errors) > 0 { +// return errors, false +// } + +// return nil, true +// } + +// func (r *RegisterDTO) validateRequiredFields(errors map[string][]string) { + +// if strings.TrimSpace(r.Username) == "" { +// errors["username"] = append(errors["username"], "Username is required") +// } +// if strings.TrimSpace(r.Name) == "" { +// errors["name"] = append(errors["name"], "Name is required") +// } +// if strings.TrimSpace(r.Phone) == "" { +// errors["phone"] = append(errors["phone"], "Phone number is required") +// } +// if strings.TrimSpace(r.Email) == "" { +// errors["email"] = append(errors["email"], "Email is required") +// } +// if strings.TrimSpace(r.Password) == "" { +// errors["password"] = append(errors["password"], "Password is required") +// } +// if strings.TrimSpace(r.ConfirmPassword) == "" { +// errors["confirm_password"] = append(errors["confirm_password"], "Confirm password is required") +// } +// if strings.TrimSpace(r.RoleID) == "" { +// errors["roleId"] = append(errors["roleId"], "RoleID is required") +// } +// } + +// func IsValidPhoneNumber(phone string) bool { + +// re := regexp.MustCompile(`^\+62\d{9,13}$`) +// return re.MatchString(phone) +// } + +// func IsValidEmail(email string) bool { + +// re := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) +// return re.MatchString(email) +// } + +// func IsValidPassword(password string) bool { +// if len(password) < 8 { +// return false +// } + +// re := regexp.MustCompile(`\d`) +// return re.MatchString(password) +// } diff --git a/dto/user_dto.go b/dto/user_dto.go index cb2dc38..b527605 100644 --- a/dto/user_dto.go +++ b/dto/user_dto.go @@ -37,11 +37,11 @@ func (r *UpdateUserDTO) Validate() (map[string][]string, bool) { errors["phone"] = append(errors["phone"], "Invalid phone number format. Use +62 followed by 9-13 digits") } - if strings.TrimSpace(r.Email) == "" { - errors["email"] = append(errors["email"], "Email is required") - } else if !IsValidEmail(r.Email) { - errors["email"] = append(errors["email"], "Invalid email format") - } + // if strings.TrimSpace(r.Email) == "" { + // errors["email"] = append(errors["email"], "Email is required") + // } else if !IsValidEmail(r.Email) { + // errors["email"] = append(errors["email"], "Invalid email format") + // } if len(errors) > 0 { return errors, false diff --git a/go.mod b/go.mod index ff6e89a..99bc202 100644 --- a/go.mod +++ b/go.mod @@ -6,17 +6,24 @@ require ( github.com/go-redis/redis/v8 v8.11.5 github.com/gofiber/fiber/v2 v2.52.5 github.com/golang-jwt/jwt/v5 v5.2.1 - github.com/google/uuid v1.5.0 + github.com/google/uuid v1.6.0 github.com/joho/godotenv v1.5.1 - golang.org/x/crypto v0.19.0 + golang.org/x/crypto v0.36.0 gorm.io/driver/postgres v1.5.11 gorm.io/gorm v1.25.12 ) require ( + golang.org/x/term v0.30.0 // indirect + rsc.io/qr v0.2.0 // indirect +) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect github.com/andybalholm/brotli v1.0.5 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/gorilla/websocket v1.5.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgx/v5 v5.5.5 // indirect @@ -24,16 +31,22 @@ require ( github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/klauspost/compress v1.17.0 // indirect + github.com/lib/pq v1.10.9 github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mdp/qrterminal/v3 v3.2.0 github.com/rivo/uniseg v0.2.0 // indirect - github.com/stretchr/testify v1.8.4 // indirect + github.com/rs/zerolog v1.33.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect - golang.org/x/net v0.21.0 // indirect - golang.org/x/sync v0.9.0 // indirect - golang.org/x/sys v0.17.0 // indirect - golang.org/x/text v0.20.0 // indirect + go.mau.fi/libsignal v0.1.2 // indirect + go.mau.fi/util v0.8.6 // indirect + go.mau.fi/whatsmeow v0.0.0-20250316144733-e7e263bf2175 + golang.org/x/net v0.37.0 // indirect + golang.org/x/sync v0.12.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.23.0 // indirect + google.golang.org/protobuf v1.36.5 ) diff --git a/go.sum b/go.sum index e2d87b7..83b4326 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,10 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -11,12 +14,17 @@ github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo= github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= -github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= @@ -33,46 +41,68 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mdp/qrterminal/v3 v3.2.0 h1:qteQMXO3oyTK4IHwj2mWsKYYRBOp1Pj2WRYFYYNTCdk= +github.com/mdp/qrterminal/v3 v3.2.0/go.mod h1:XGGuua4Lefrl7TLEsSONiD+UEjQXJZ4mPzF+gWYIJkk= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= +github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= -golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= -golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +go.mau.fi/libsignal v0.1.2 h1:Vs16DXWxSKyzVtI+EEXLCSy5pVWzzCzp/2eqFGvLyP0= +go.mau.fi/libsignal v0.1.2/go.mod h1:JpnLSSJptn/s1sv7I56uEMywvz8x4YzxeF5OzdPb6PE= +go.mau.fi/util v0.8.6 h1:AEK13rfgtiZJL2YsNK+W4ihhYCuukcRom8WPP/w/L54= +go.mau.fi/util v0.8.6/go.mod h1:uNB3UTXFbkpp7xL1M/WvQks90B/L4gvbLpbS0603KOE= +go.mau.fi/whatsmeow v0.0.0-20250316144733-e7e263bf2175 h1:BDShdc10qJzi3B0xPGA6HVQl+929wIFst8/W+8EnvbI= +go.mau.fi/whatsmeow v0.0.0-20250316144733-e7e263bf2175/go.mod h1:WNhj4JeQ6YR6dUOEiCXKqmE4LavSFkwRoKmu4atRrRs= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= -golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= @@ -85,3 +115,5 @@ gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314= gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= +rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= +rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= diff --git a/internal/handler/auth_handler.go b/internal/handler/auth_handler.go index 3f48822..50edbe0 100644 --- a/internal/handler/auth_handler.go +++ b/internal/handler/auth_handler.go @@ -1,72 +1,88 @@ package handler import ( - "log" - "github.com/gofiber/fiber/v2" "github.com/pahmiudahgede/senggoldong/dto" "github.com/pahmiudahgede/senggoldong/internal/services" - "github.com/pahmiudahgede/senggoldong/utils" ) -type UserHandler struct { - UserService services.UserService +type AuthHandler struct { + AuthService services.AuthService } -func NewUserHandler(userService services.UserService) *UserHandler { - return &UserHandler{UserService: userService} +func NewAuthHandler(authService services.AuthService) *AuthHandler { + return &AuthHandler{AuthService: authService} } -func (h *UserHandler) Login(c *fiber.Ctx) error { - var loginDTO dto.LoginDTO - if err := c.BodyParser(&loginDTO); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}}) +func (h *AuthHandler) Register(c *fiber.Ctx) error { + var request dto.RegisterRequest + + if err := c.BodyParser(&request); err != nil { + return c.Status(400).SendString("Invalid input") } - validationErrors, valid := loginDTO.Validate() - if !valid { - return utils.ValidationErrorResponse(c, validationErrors) + if errors, valid := request.Validate(); !valid { + return c.Status(400).JSON(errors) } - user, err := h.UserService.Login(loginDTO) + _, err := h.AuthService.RegisterUser(request) if err != nil { - return utils.GenericResponse(c, fiber.StatusUnauthorized, err.Error()) + return c.Status(500).SendString(err.Error()) } - return utils.SuccessResponse(c, user, "Login successful") + return c.Status(201).JSON(fiber.Map{ + "meta": fiber.Map{ + "status": 201, + "message": "The input register from the user has been successfully recorded. Please check the otp code sent to your number.", + }, + }) } -func (h *UserHandler) Register(c *fiber.Ctx) error { - - var registerDTO dto.RegisterDTO - if err := c.BodyParser(®isterDTO); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid request body"}}) +func (h *AuthHandler) VerifyOTP(c *fiber.Ctx) error { + var request struct { + Phone string `json:"phone"` + OTP string `json:"otp"` } - errors, valid := registerDTO.Validate() - if !valid { - return utils.ValidationErrorResponse(c, errors) + if err := c.BodyParser(&request); err != nil { + return c.Status(400).SendString("Invalid input") } - userResponse, err := h.UserService.Register(registerDTO) + err := h.AuthService.VerifyOTP(request.Phone, request.OTP) if err != nil { - return utils.GenericResponse(c, fiber.StatusConflict, err.Error()) + return c.Status(400).JSON(dto.Response{ + Meta: dto.MetaResponse{ + Status: 400, + Message: "Invalid OTP", + }, + Data: nil, + }) } - return utils.CreateResponse(c, userResponse, "Registration successful") -} - -func (h *UserHandler) Logout(c *fiber.Ctx) error { - userID, ok := c.Locals("userID").(string) - if !ok || userID == "" { - log.Println("Unauthorized access: User ID not found in session") - return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") - } - - err := utils.DeleteSessionData(userID) + user, err := h.AuthService.GetUserByPhone(request.Phone) if err != nil { - return utils.InternalServerErrorResponse(c, "Error logging out") + return c.Status(500).SendString("Error retrieving user") + } + if user == nil { + return c.Status(404).SendString("User not found") } - return utils.SuccessResponse(c, nil, "Logout successful") + token, err := h.AuthService.GenerateJWT(user) + if err != nil { + return c.Status(500).SendString("Error generating token") + } + + response := dto.Response{ + Meta: dto.MetaResponse{ + Status: 200, + Message: "OTP yang dimasukkan valid", + }, + Data: &dto.UserDataResponse{ + UserID: user.ID, + UserRole: user.Role.RoleName, + Token: token, + }, + } + + return c.Status(200).JSON(response) } diff --git a/internal/handler/user_handler.go b/internal/handler/user_handler.go index c4f60d5..f8e0d73 100644 --- a/internal/handler/user_handler.go +++ b/internal/handler/user_handler.go @@ -97,29 +97,29 @@ func (h *UserProfileHandler) UpdateUserProfile(c *fiber.Ctx) error { return utils.SuccessResponse(c, userResponse, "User profile updated successfully") } -func (h *UserProfileHandler) UpdateUserPassword(c *fiber.Ctx) error { - var passwordData dto.UpdatePasswordDTO - if err := c.BodyParser(&passwordData); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}}) - } +// func (h *UserProfileHandler) UpdateUserPassword(c *fiber.Ctx) error { +// var passwordData dto.UpdatePasswordDTO +// if err := c.BodyParser(&passwordData); err != nil { +// return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}}) +// } - userID, ok := c.Locals("userID").(string) - if !ok || userID == "" { - return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") - } +// userID, ok := c.Locals("userID").(string) +// if !ok || userID == "" { +// return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") +// } - errors, valid := passwordData.Validate() - if !valid { - return utils.ValidationErrorResponse(c, errors) - } +// errors, valid := passwordData.Validate() +// if !valid { +// return utils.ValidationErrorResponse(c, errors) +// } - message, err := h.UserProfileService.UpdateUserPassword(userID, passwordData) - if err != nil { - return utils.GenericResponse(c, fiber.StatusBadRequest, err.Error()) - } +// message, err := h.UserProfileService.UpdateUserPassword(userID, passwordData) +// if err != nil { +// return utils.GenericResponse(c, fiber.StatusBadRequest, err.Error()) +// } - return utils.GenericResponse(c, fiber.StatusOK, message) -} +// return utils.GenericResponse(c, fiber.StatusOK, message) +// } func (h *UserProfileHandler) UpdateUserAvatar(c *fiber.Ctx) error { userID, ok := c.Locals("userID").(string) diff --git a/internal/repositories/auth_repo.go b/internal/repositories/auth_repo.go index 2b0622f..5ffe64d 100644 --- a/internal/repositories/auth_repo.go +++ b/internal/repositories/auth_repo.go @@ -1,20 +1,14 @@ package repositories import ( - "fmt" - "github.com/pahmiudahgede/senggoldong/model" "gorm.io/gorm" ) type UserRepository interface { - FindByIdentifierAndRole(identifier, roleID string) (*model.User, error) - FindByEmailOrUsernameOrPhone(identifier string) (*model.User, error) - FindByUsername(username string) (*model.User, error) + FindByPhone(phone string) (*model.User, error) FindByPhoneAndRole(phone, roleID string) (*model.User, error) - FindByEmailAndRole(email, roleID string) (*model.User, error) - - Create(user *model.User) error + CreateUser(user *model.User) error } type userRepository struct { @@ -25,22 +19,14 @@ func NewUserRepository(db *gorm.DB) UserRepository { return &userRepository{DB: db} } -func (r *userRepository) FindByIdentifierAndRole(identifier, roleID string) (*model.User, error) { +func (r *userRepository) FindByPhone(phone string) (*model.User, error) { var user model.User - err := r.DB.Preload("Role").Where("(email = ? OR username = ? OR phone = ?) AND role_id = ?", identifier, identifier, identifier, roleID).First(&user).Error - if err != nil { - return nil, err - } - if user.Role == nil { - return nil, fmt.Errorf("role not found for this user") - } - return &user, nil -} -func (r *userRepository) FindByUsername(username string) (*model.User, error) { - var user model.User - err := r.DB.Where("username = ?", username).First(&user).Error + err := r.DB.Preload("Role").Where("phone = ?", phone).First(&user).Error if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, nil + } return nil, err } return &user, nil @@ -55,28 +41,6 @@ func (r *userRepository) FindByPhoneAndRole(phone, roleID string) (*model.User, return &user, nil } -func (r *userRepository) FindByEmailAndRole(email, roleID string) (*model.User, error) { - var user model.User - err := r.DB.Where("email = ? AND role_id = ?", email, roleID).First(&user).Error - if err != nil { - return nil, err - } - return &user, nil -} - -func (r *userRepository) FindByEmailOrUsernameOrPhone(identifier string) (*model.User, error) { - var user model.User - err := r.DB.Where("email = ? OR username = ? OR phone = ?", identifier, identifier, identifier).First(&user).Error - if err != nil { - return nil, err - } - return &user, nil -} - -func (r *userRepository) Create(user *model.User) error { - err := r.DB.Create(user).Error - if err != nil { - return err - } - return nil +func (r *userRepository) CreateUser(user *model.User) error { + return r.DB.Create(user).Error } diff --git a/internal/services/auth_service.go b/internal/services/auth_service.go index 6be1bba..93bc0c2 100644 --- a/internal/services/auth_service.go +++ b/internal/services/auth_service.go @@ -1,171 +1,134 @@ package services import ( - "errors" "fmt" "time" + "github.com/go-redis/redis/v8" "github.com/golang-jwt/jwt/v5" + "github.com/pahmiudahgede/senggoldong/config" "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" ) -const ( - ErrUsernameTaken = "username is already taken" - ErrPhoneTaken = "phone number is already used for this role" - ErrEmailTaken = "email is already used for this role" - ErrInvalidRoleID = "invalid roleId" - ErrPasswordMismatch = "password and confirm password do not match" - ErrRoleIDRequired = "roleId is required" - ErrFailedToHashPassword = "failed to hash password" - ErrFailedToCreateUser = "failed to create user" - ErrIncorrectPassword = "incorrect password" - ErrAccountNotFound = "account not found" -) - -type UserService interface { - Login(credentials dto.LoginDTO) (*dto.UserResponseWithToken, error) - Register(user dto.RegisterDTO) (*dto.UserResponseDTO, error) +type AuthService interface { + RegisterUser(request dto.RegisterRequest) (*model.User, error) + VerifyOTP(phone, otp string) error + GetUserByPhone(phone string) (*model.User, error) + GenerateJWT(user *model.User) (string, error) } -type userService struct { - UserRepo repositories.UserRepository - RoleRepo repositories.RoleRepository - SecretKey string +type authService struct { + UserRepo repositories.UserRepository } -func NewUserService(userRepo repositories.UserRepository, roleRepo repositories.RoleRepository, secretKey string) UserService { - return &userService{UserRepo: userRepo, RoleRepo: roleRepo, SecretKey: secretKey} +func NewAuthService(userRepo repositories.UserRepository) AuthService { + return &authService{UserRepo: userRepo} } -func (s *userService) Login(credentials dto.LoginDTO) (*dto.UserResponseWithToken, error) { - if credentials.RoleID == "" { - return nil, errors.New(ErrRoleIDRequired) +func (s *authService) RegisterUser(request dto.RegisterRequest) (*model.User, error) { + + user, err := s.UserRepo.FindByPhone(request.Phone) + if err == nil && user != nil { + return nil, fmt.Errorf("user with phone %s already exists", request.Phone) } - user, err := s.UserRepo.FindByIdentifierAndRole(credentials.Identifier, credentials.RoleID) + user = &model.User{ + Phone: request.Phone, + RoleID: request.RoleID, + EmailVerified: false, + } + + err = s.UserRepo.CreateUser(user) if err != nil { - return nil, errors.New(ErrAccountNotFound) + return nil, fmt.Errorf("failed to create user: %v", err) } - if !CheckPasswordHash(credentials.Password, user.Password) { - return nil, errors.New(ErrIncorrectPassword) - } - - token, err := s.generateJWT(user) + _, err = s.SendOTP(request.Phone) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to send OTP: %v", err) } - sessionKey := fmt.Sprintf("session:%s", user.ID) - sessionData := map[string]interface{}{ - "userID": user.ID, - "roleID": user.RoleID, - "roleName": user.Role.RoleName, - } - - err = utils.SetJSONData(sessionKey, sessionData, time.Hour*24) - if err != nil { - return nil, err - } - - return &dto.UserResponseWithToken{ - RoleName: user.Role.RoleName, - UserID: user.ID, - Token: token, - }, nil + return user, nil } -func (s *userService) generateJWT(user *model.User) (string, error) { +func (s *authService) GetUserByPhone(phone string) (*model.User, error) { + user, err := s.UserRepo.FindByPhone(phone) + if err != nil { + return nil, fmt.Errorf("error retrieving user by phone: %v", err) + } + if user == nil { + return nil, fmt.Errorf("user not found") + } + return user, nil +} + +func (s *authService) SendOTP(phone string) (string, error) { + otpCode := generateOTP() + + message := fmt.Sprintf("Your OTP code is: %s", otpCode) + err := config.SendWhatsAppMessage(phone, message) + if err != nil { + return "", fmt.Errorf("failed to send OTP via WhatsApp: %v", err) + } + + expirationTime := 5 * time.Minute + 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 otpCode, nil +} + +func (s *authService) VerifyOTP(phone, otp string) error { + + otpRecord, err := config.RedisClient.Get(config.Ctx, phone).Result() + if err == redis.Nil { + + return fmt.Errorf("OTP not found or expired") + } else if err != nil { + + return fmt.Errorf("failed to retrieve OTP from Redis: %v", err) + } + + if otp != otpRecord { + return fmt.Errorf("invalid OTP") + } + + err = config.RedisClient.Del(config.Ctx, phone).Err() + if err != nil { + return fmt.Errorf("failed to delete OTP from Redis: %v", err) + } + + return nil +} + +func (s *authService) GenerateJWT(user *model.User) (string, error) { + if user == nil || user.Role == nil { + return "", fmt.Errorf("user or user role is nil, cannot generate token") + } + claims := jwt.MapClaims{ - "sub": user.ID, - "iat": time.Now().Unix(), - "exp": time.Now().Add(time.Hour * 24).Unix(), + "sub": user.ID, + "role": user.Role.RoleName, + "iat": time.Now().Unix(), + "exp": time.Now().Add(time.Hour * 24).Unix(), } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - tokenString, err := token.SignedString([]byte(s.SecretKey)) + secretKey := config.GetSecretKey() + + tokenString, err := token.SignedString([]byte(secretKey)) if err != nil { - return "", err + return "", fmt.Errorf("failed to generate JWT token: %v", err) } return tokenString, nil } -func CheckPasswordHash(password, hashedPassword string) bool { - err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) - return err == nil -} - -func (s *userService) Register(user dto.RegisterDTO) (*dto.UserResponseDTO, error) { - - if user.Password != user.ConfirmPassword { - return nil, fmt.Errorf("%s", ErrPasswordMismatch) - } - - if user.RoleID == "" { - return nil, fmt.Errorf("%s", ErrRoleIDRequired) - } - - role, err := s.RoleRepo.FindByID(user.RoleID) - if err != nil { - return nil, fmt.Errorf("%s: %v", ErrInvalidRoleID, err) - } - - if existingUser, _ := s.UserRepo.FindByUsername(user.Username); existingUser != nil { - return nil, fmt.Errorf("%s", ErrUsernameTaken) - } - - if existingPhone, _ := s.UserRepo.FindByPhoneAndRole(user.Phone, user.RoleID); existingPhone != nil { - return nil, fmt.Errorf("%s", ErrPhoneTaken) - } - - if existingEmail, _ := s.UserRepo.FindByEmailAndRole(user.Email, user.RoleID); existingEmail != nil { - return nil, fmt.Errorf("%s", ErrEmailTaken) - } - - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost) - if err != nil { - return nil, fmt.Errorf("%s: %v", ErrFailedToHashPassword, err) - } - - newUser := model.User{ - Username: user.Username, - Name: user.Name, - Phone: user.Phone, - Email: user.Email, - Password: string(hashedPassword), - RoleID: user.RoleID, - } - - err = s.UserRepo.Create(&newUser) - if err != nil { - return nil, fmt.Errorf("%s: %v", ErrFailedToCreateUser, err) - } - - userResponse := s.prepareUserResponse(newUser, role) - - return userResponse, nil -} - -func (s *userService) prepareUserResponse(user model.User, role *model.Role) *dto.UserResponseDTO { - - createdAt, _ := utils.FormatDateToIndonesianFormat(user.CreatedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(user.UpdatedAt) - - return &dto.UserResponseDTO{ - ID: user.ID, - Username: user.Username, - Name: user.Name, - Phone: user.Phone, - Email: user.Email, - EmailVerified: user.EmailVerified, - RoleName: role.RoleName, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - } +func generateOTP() string { + return fmt.Sprintf("%06d", time.Now().UnixNano()%1000000) } diff --git a/internal/services/user_service.go b/internal/services/user_service.go index 3b8b092..1f5b54e 100644 --- a/internal/services/user_service.go +++ b/internal/services/user_service.go @@ -14,7 +14,7 @@ import ( "github.com/pahmiudahgede/senggoldong/internal/repositories" "github.com/pahmiudahgede/senggoldong/model" "github.com/pahmiudahgede/senggoldong/utils" - "golang.org/x/crypto/bcrypt" + // "golang.org/x/crypto/bcrypt" ) var allowedExtensions = []string{".jpg", ".jpeg", ".png"} @@ -22,7 +22,7 @@ var allowedExtensions = []string{".jpg", ".jpeg", ".png"} type UserProfileService interface { GetUserProfile(userID string) (*dto.UserResponseDTO, error) UpdateUserProfile(userID string, updateData dto.UpdateUserDTO) (*dto.UserResponseDTO, error) - UpdateUserPassword(userID string, passwordData dto.UpdatePasswordDTO) (string, error) + // UpdateUserPassword(userID string, passwordData dto.UpdatePasswordDTO) (string, error) UpdateUserAvatar(userID string, file *multipart.FileHeader) (string, error) GetAllUsers() ([]dto.UserResponseDTO, error) @@ -162,12 +162,12 @@ func (s *userProfileService) UpdateUserProfile(userID string, updateData dto.Upd user.Phone = updateData.Phone } - if updateData.Email != "" && updateData.Email != user.Email { - if err := s.updateEmailIfNeeded(user, updateData.Email); err != nil { - return nil, err - } - user.Email = updateData.Email - } + // if updateData.Email != "" && updateData.Email != user.Email { + // if err := s.updateEmailIfNeeded(user, updateData.Email); err != nil { + // return nil, err + // } + // user.Email = updateData.Email + // } err = s.UserProfileRepo.Update(user) if err != nil { @@ -196,43 +196,43 @@ func (s *userProfileService) updatePhoneIfNeeded(user *model.User, newPhone stri return nil } -func (s *userProfileService) updateEmailIfNeeded(user *model.User, newEmail string) error { - existingEmail, _ := s.UserRepo.FindByEmailAndRole(newEmail, user.RoleID) - if existingEmail != nil { - return fmt.Errorf("email is already used for this role") - } - return nil -} +// func (s *userProfileService) updateEmailIfNeeded(user *model.User, newEmail string) error { +// existingEmail, _ := s.UserRepo.FindByEmailAndRole(newEmail, user.RoleID) +// if existingEmail != nil { +// return fmt.Errorf("email is already used for this role") +// } +// return nil +// } -func (s *userProfileService) UpdateUserPassword(userID string, passwordData dto.UpdatePasswordDTO) (string, error) { +// func (s *userProfileService) UpdateUserPassword(userID string, passwordData dto.UpdatePasswordDTO) (string, error) { - validationErrors, valid := passwordData.Validate() - if !valid { - return "", fmt.Errorf("validation failed: %v", validationErrors) - } +// validationErrors, valid := passwordData.Validate() +// if !valid { +// return "", fmt.Errorf("validation failed: %v", validationErrors) +// } - user, err := s.UserProfileRepo.FindByID(userID) - if err != nil { - return "", errors.New("user not found") - } +// user, err := s.UserProfileRepo.FindByID(userID) +// if err != nil { +// return "", errors.New("user not found") +// } - if !CheckPasswordHash(passwordData.OldPassword, user.Password) { - return "", errors.New("old password is incorrect") - } +// if !CheckPasswordHash(passwordData.OldPassword, user.Password) { +// return "", errors.New("old password is incorrect") +// } - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(passwordData.NewPassword), bcrypt.DefaultCost) - if err != nil { - return "", fmt.Errorf("failed to hash new password: %v", err) - } +// hashedPassword, err := bcrypt.GenerateFromPassword([]byte(passwordData.NewPassword), bcrypt.DefaultCost) +// if err != nil { +// return "", fmt.Errorf("failed to hash new password: %v", err) +// } - user.Password = string(hashedPassword) - err = s.UserProfileRepo.Update(user) - if err != nil { - return "", fmt.Errorf("failed to update password: %v", err) - } +// user.Password = string(hashedPassword) +// err = s.UserProfileRepo.Update(user) +// if err != nil { +// return "", fmt.Errorf("failed to update password: %v", err) +// } - return "Password berhasil diupdate", nil -} +// return "Password berhasil diupdate", nil +// } func (s *userProfileService) UpdateUserAvatar(userID string, file *multipart.FileHeader) (string, error) { baseURL := os.Getenv("BASE_URL") diff --git a/presentation/auth_route.go b/presentation/auth_route.go index 138992e..16ccc7f 100644 --- a/presentation/auth_route.go +++ b/presentation/auth_route.go @@ -1,31 +1,40 @@ package presentation import ( - "log" - "os" - "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" + // "gorm.io/gorm" + // "github.com/pahmiudahgede/senggoldong/middleware" ) func AuthRouter(api fiber.Router) { - secretKey := os.Getenv("SECRET_KEY") - if secretKey == "" { - log.Fatal("SECRET_KEY is not set in the environment variables") - os.Exit(1) - } + // 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) - roleRepo := repositories.NewRoleRepository(config.DB) - userService := services.NewUserService(userRepo, roleRepo, secretKey) - userHandler := handler.NewUserHandler(userService) + authService := services.NewAuthService(userRepo) - api.Post("/login", userHandler.Login) - api.Post("/register", userHandler.Register) - api.Post("/logout", middleware.AuthMiddleware, userHandler.Logout) + authHandler := handler.NewAuthHandler(authService) + // Routes + api.Post("/register", authHandler.Register) + api.Post("/verify-otp", authHandler.VerifyOTP) } diff --git a/presentation/user_route.go b/presentation/user_route.go index dcdb00c..2e54a2e 100644 --- a/presentation/user_route.go +++ b/presentation/user_route.go @@ -23,6 +23,6 @@ func UserProfileRouter(api fiber.Router) { userProfilRoute.Get("/:roleid", middleware.AuthMiddleware, userProfileHandler.GetUsersByRoleID) userProfilRoute.Put("/update-user", middleware.AuthMiddleware, userProfileHandler.UpdateUserProfile) - userProfilRoute.Patch("/update-user-password", middleware.AuthMiddleware, userProfileHandler.UpdateUserPassword) + // userProfilRoute.Patch("/update-user-password", middleware.AuthMiddleware, userProfileHandler.UpdateUserPassword) userProfilRoute.Patch("/upload-photoprofile", middleware.AuthMiddleware, userProfileHandler.UpdateUserAvatar) } diff --git a/utils/redis_caching.go b/utils/redis_caching.go index 24e748e..0cbf2ff 100644 --- a/utils/redis_caching.go +++ b/utils/redis_caching.go @@ -97,3 +97,44 @@ func logAndReturnError(message string, err error) error { log.Printf("%s: %v", message, err) return err } + +func SetStringData(key, value string, expiration time.Duration) error { + if expiration == 0 { + expiration = defaultExpiration + } + + err := config.RedisClient.Set(ctx, key, value, expiration).Err() + if err != nil { + return logAndReturnError(fmt.Sprintf("Error setting string data in Redis with key: %s", key), err) + } + + log.Printf("String data stored in Redis with key: %s", key) + return nil +} + +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 "", logAndReturnError(fmt.Sprintf("Error retrieving string data from Redis with key: %s", 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 +} \ No newline at end of file