update: completely auth statemrnt and session management
This commit is contained in:
parent
ba4645fef9
commit
e65d6383fc
|
@ -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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
@ -33,3 +34,15 @@ func (r *userRepository) GetUserByPhone(phone string) (*model.User, error) {
|
||||||
}
|
}
|
||||||
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
|
||||||
|
}
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue