update: completely auth statemrnt and session management

This commit is contained in:
pahmiudahgede 2025-03-24 05:46:54 +07:00
parent ba4645fef9
commit e65d6383fc
5 changed files with 178 additions and 95 deletions

View File

@ -11,7 +11,8 @@ type RegisterRequest struct {
} }
type VerifyOTPRequest struct { type VerifyOTPRequest struct {
Phone string `json:"phone"` RoleID string `json:"role_id"`
Phone string `json:"phone"`
OTP string `json:"otp"` OTP string `json:"otp"`
} }

View File

@ -1,6 +1,7 @@
package handler package handler
import ( import (
"log"
"rijig/dto" "rijig/dto"
"rijig/internal/services" "rijig/internal/services"
"rijig/utils" "rijig/utils"
@ -16,34 +17,64 @@ func NewAuthHandler(authService services.AuthService) *AuthHandler {
return &AuthHandler{authService} return &AuthHandler{authService}
} }
func (h *AuthHandler) RegisterUser(c *fiber.Ctx) error { func (h *AuthHandler) RegisterOrLoginHandler(c *fiber.Ctx) error {
var req dto.RegisterRequest var req dto.RegisterRequest
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
return utils.ErrorResponse(c, "Invalid request body") return utils.ErrorResponse(c, "Invalid request body")
} }
if errors, valid := req.Validate(); !valid { if req.Phone == "" || req.RoleID == "" {
return utils.ValidationErrorResponse(c, errors) return utils.ErrorResponse(c, "Phone number and role ID are required")
} }
err := h.authService.RegisterUser(&req) if err := h.authService.RegisterOrLogin(&req); err != nil {
if err != nil {
return utils.ErrorResponse(c, err.Error()) return utils.ErrorResponse(c, err.Error())
} }
return utils.SuccessResponse(c, nil, "Kode OTP telah dikirimkan ke nomor WhatsApp anda") return utils.SuccessResponse(c, nil, "OTP sent successfully")
} }
func (h *AuthHandler) VerifyOTP(c *fiber.Ctx) error { func (h *AuthHandler) VerifyOTPHandler(c *fiber.Ctx) error {
var req dto.VerifyOTPRequest var req dto.VerifyOTPRequest
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
return utils.ErrorResponse(c, "Invalid request body") return utils.ErrorResponse(c, "Invalid request body")
} }
if req.OTP == "" {
return utils.ErrorResponse(c, "OTP is required")
}
response, err := h.authService.VerifyOTP(&req) response, err := h.authService.VerifyOTP(&req)
if err != nil { if err != nil {
return utils.ErrorResponse(c, err.Error()) return utils.ErrorResponse(c, err.Error())
} }
return utils.SuccessResponse(c, response, "Registration successful") return utils.SuccessResponse(c, response, "Registration/Login successful")
}
func (h *AuthHandler) LogoutHandler(c *fiber.Ctx) error {
userID, ok := c.Locals("userID").(string)
if !ok || userID == "" {
return utils.ErrorResponse(c, "User is not logged in or invalid session")
}
phoneKey := "user_phone:" + userID
phone, err := utils.GetStringData(phoneKey)
if err != nil || phone == "" {
log.Printf("Error retrieving phone from Redis for user %s: %v", userID, err)
return utils.ErrorResponse(c, "Phone number is missing or invalid session data")
}
err = h.authService.Logout(userID, phone)
if err != nil {
log.Printf("Error during logout process for user %s: %v", userID, err)
return utils.ErrorResponse(c, err.Error())
}
return utils.SuccessResponse(c, nil, "Logged out successfully")
} }

View File

@ -9,6 +9,7 @@ import (
type UserRepository interface { type UserRepository interface {
CreateUser(user *model.User) (*model.User, error) CreateUser(user *model.User) (*model.User, error)
GetUserByPhone(phone string) (*model.User, error) GetUserByPhone(phone string) (*model.User, error)
GetUserByPhoneAndRole(phone string, roleID string) (*model.User, error)
} }
type userRepository struct { type userRepository struct {
@ -32,4 +33,16 @@ func (r *userRepository) GetUserByPhone(phone string) (*model.User, error) {
return nil, err return nil, err
} }
return &user, nil return &user, nil
} }
func (r *userRepository) GetUserByPhoneAndRole(phone string, roleID string) (*model.User, error) {
var user model.User
err := r.db.Where("phone = ? AND role_id = ?", phone, roleID).First(&user).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return nil, err
}
return &user, nil
}

View File

@ -12,12 +12,14 @@ import (
"time" "time"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
) )
const otpCooldown = 30 * time.Second
type AuthService interface { type AuthService interface {
RegisterUser(req *dto.RegisterRequest) error RegisterOrLogin(req *dto.RegisterRequest) error
VerifyOTP(req *dto.VerifyOTPRequest) (*dto.UserDataResponse, error) VerifyOTP(req *dto.VerifyOTPRequest) (*dto.UserDataResponse, error)
Logout(userID, phone string) error
} }
type authService struct { type authService struct {
@ -29,77 +31,66 @@ func NewAuthService(userRepo repositories.UserRepository, roleRepo repositories.
return &authService{userRepo, roleRepo} return &authService{userRepo, roleRepo}
} }
const otpCooldown = 30 func (s *authService) RegisterOrLogin(req *dto.RegisterRequest) error {
func (s *authService) RegisterUser(req *dto.RegisterRequest) error { if err := s.checkOTPRequestCooldown(req.Phone); err != nil {
return err
user, err := s.userRepo.GetUserByPhone(req.Phone)
if err == nil && user != nil {
return errors.New("phone number already registered")
} }
lastOtpSent, err := utils.GetStringData("otp_sent:" + req.Phone) user, err := s.userRepo.GetUserByPhoneAndRole(req.Phone, req.RoleID)
if err == nil && lastOtpSent != "" { if err != nil {
lastSentTime, err := time.Parse(time.RFC3339, lastOtpSent) return fmt.Errorf("failed to check existing user: %w", err)
if err != nil {
return errors.New("invalid OTP sent timestamp")
}
if time.Since(lastSentTime).Seconds() < otpCooldown {
return errors.New("please wait before requesting another OTP")
}
} }
userID := uuid.New().String() if user != nil {
return s.sendOTP(req.Phone)
}
user = &model.User{ user = &model.User{
Phone: req.Phone, Phone: req.Phone,
RoleID: req.RoleID, RoleID: req.RoleID,
} }
err = utils.SetJSONData("user:"+userID, user, 10*time.Minute) createdUser, err := s.userRepo.CreateUser(user)
if err != nil { if err != nil {
return fmt.Errorf("failed to create new user: %w", err)
}
if err := s.saveUserToRedis(createdUser.ID, createdUser, req.Phone); err != nil {
return err return err
} }
err = utils.SetStringData("user_phone:"+req.Phone, userID, 10*time.Minute) return s.sendOTP(req.Phone)
if err != nil { }
return err
func (s *authService) checkOTPRequestCooldown(phone string) error {
otpSentTime, err := utils.GetStringData("otp_sent:" + phone)
if err != nil || otpSentTime == "" {
return nil
} }
lastSent, _ := time.Parse(time.RFC3339, otpSentTime)
otp := generateOTP() if time.Since(lastSent) < otpCooldown {
return errors.New("please wait before requesting a new OTP")
err = config.SendWhatsAppMessage(req.Phone, fmt.Sprintf("Your OTP is: %s", otp))
if err != nil {
return err
} }
err = utils.SetStringData("otp:"+req.Phone, otp, 10*time.Minute)
if err != nil {
return err
}
err = utils.SetStringData("otp_sent:"+req.Phone, time.Now().Format(time.RFC3339), 10*time.Minute)
if err != nil {
return err
}
return nil return nil
} }
func (s *authService) sendOTP(phone string) error {
otp := generateOTP()
if err := config.SendWhatsAppMessage(phone, fmt.Sprintf("Your OTP is: %s", otp)); err != nil {
return err
}
if err := utils.SetStringData("otp:"+phone, otp, 10*time.Minute); err != nil {
return err
}
return utils.SetStringData("otp_sent:"+phone, time.Now().Format(time.RFC3339), 10*time.Minute)
}
func (s *authService) VerifyOTP(req *dto.VerifyOTPRequest) (*dto.UserDataResponse, error) { func (s *authService) VerifyOTP(req *dto.VerifyOTPRequest) (*dto.UserDataResponse, error) {
isLoggedIn, err := utils.GetStringData("user_logged_in:" + req.Phone)
if err == nil && isLoggedIn == "true" {
return nil, errors.New("you are already logged in")
}
storedOTP, err := utils.GetStringData("otp:" + req.Phone) storedOTP, err := utils.GetStringData("otp:" + req.Phone)
if err != nil { if err != nil || storedOTP == "" {
return nil, err
}
if storedOTP == "" {
return nil, errors.New("OTP expired or not found") return nil, errors.New("OTP expired or not found")
} }
@ -107,71 +98,116 @@ func (s *authService) VerifyOTP(req *dto.VerifyOTPRequest) (*dto.UserDataRespons
return nil, errors.New("invalid OTP") return nil, errors.New("invalid OTP")
} }
userID, err := utils.GetStringData("user_phone:" + req.Phone) if err := utils.DeleteData("otp:" + req.Phone); err != nil {
if err != nil || userID == "" { return nil, fmt.Errorf("failed to remove OTP from Redis: %w", err)
return nil, errors.New("user data not found in Redis")
} }
userData, err := utils.GetJSONData("user:" + userID) existingUser, err := s.userRepo.GetUserByPhoneAndRole(req.Phone, req.RoleID)
if err != nil || userData == nil { if err != nil {
return nil, errors.New("user data not found in Redis") return nil, fmt.Errorf("failed to check existing user: %w", err)
} }
user := &model.User{ var user *model.User
Phone: userData["phone"].(string), if existingUser != nil {
RoleID: userData["roleId"].(string), user = existingUser
} else {
user = &model.User{
Phone: req.Phone,
RoleID: req.RoleID,
}
createdUser, err := s.userRepo.CreateUser(user)
if err != nil {
return nil, err
}
user = createdUser
} }
createdUser, err := s.userRepo.CreateUser(user) token, err := s.generateJWTToken(user.ID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
role, err := s.roleRepo.FindByID(createdUser.RoleID) role, err := s.roleRepo.FindByID(user.RoleID)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to get role: %w", err)
} }
token, err := generateJWTToken(createdUser.ID) if err := s.saveSessionData(user.ID, user.RoleID, role.RoleName, token); err != nil {
if err != nil {
return nil, err
}
err = utils.SetStringData("user_logged_in:"+req.Phone, "true", 0)
if err != nil {
return nil, err return nil, err
} }
return &dto.UserDataResponse{ return &dto.UserDataResponse{
UserID: createdUser.ID, UserID: user.ID,
UserRole: role.RoleName, UserRole: role.RoleName,
Token: token, Token: token,
}, nil }, nil
} }
func generateOTP() string { func (s *authService) saveUserToRedis(userID string, user *model.User, phone string) error {
rand.Seed(time.Now().UnixNano()) if err := utils.SetJSONData("user:"+userID, user, 10*time.Minute); err != nil {
otp := fmt.Sprintf("%06d", rand.Intn(1000000)) return fmt.Errorf("failed to store user data in Redis: %w", err)
return otp }
if err := utils.SetStringData("user_phone:"+userID, phone, 10*time.Minute); err != nil {
return fmt.Errorf("failed to store user phone in Redis: %w", err)
}
return nil
} }
func generateJWTToken(userID string) (string, error) { func (s *authService) generateJWTToken(userID string) (string, error) {
expirationTime := time.Now().Add(24 * time.Hour) expirationTime := time.Now().Add(24 * time.Hour)
claims := &jwt.RegisteredClaims{ claims := &jwt.RegisteredClaims{
Issuer: userID, Subject: userID,
ExpiresAt: jwt.NewNumericDate(expirationTime), ExpiresAt: jwt.NewNumericDate(expirationTime),
} }
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
secretKey := config.GetSecretKey() secretKey := config.GetSecretKey()
signedToken, err := token.SignedString([]byte(secretKey)) return token.SignedString([]byte(secretKey))
if err != nil { }
return "", err
func (s *authService) saveSessionData(userID string, roleID string, roleName string, token string) error {
sessionKey := fmt.Sprintf("session:%s", userID)
sessionData := map[string]interface{}{
"userID": userID,
"roleID": roleID,
"roleName": roleName,
} }
return signedToken, nil if err := utils.SetJSONData(sessionKey, sessionData, 24*time.Hour); err != nil {
return fmt.Errorf("failed to set session data: %w", err)
}
if err := utils.SetStringData("session_token:"+userID, token, 24*time.Hour); err != nil {
return fmt.Errorf("failed to set session token: %w", err)
}
return nil
}
func (s *authService) Logout(userID, phone string) error {
keys := []string{
"session:" + userID,
"session_token:" + userID,
"user_logged_in:" + userID,
"user:" + userID,
"user_phone:" + userID,
"otp_sent:" + phone,
}
for _, key := range keys {
if err := utils.DeleteData(key); err != nil {
return fmt.Errorf("failed to delete key %s from Redis: %w", key, err)
}
}
return nil
}
func generateOTP() string {
randGenerator := rand.New(rand.NewSource(time.Now().UnixNano()))
return fmt.Sprintf("%04d", randGenerator.Intn(10000))
} }

View File

@ -5,6 +5,7 @@ import (
"rijig/internal/handler" "rijig/internal/handler"
"rijig/internal/repositories" "rijig/internal/repositories"
"rijig/internal/services" "rijig/internal/services"
"rijig/middleware"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
@ -16,6 +17,7 @@ func AuthRouter(api fiber.Router) {
authHandler := handler.NewAuthHandler(authService) authHandler := handler.NewAuthHandler(authService)
api.Post("/register", authHandler.RegisterUser) api.Post("/auth", authHandler.RegisterOrLoginHandler)
api.Post("/verify-otp", authHandler.VerifyOTP) api.Post("/logout", middleware.AuthMiddleware, authHandler.LogoutHandler)
api.Post("/verify-otp", authHandler.VerifyOTPHandler)
} }