From 53328a0bde41857340f78cd4e6f09b5ca36bb614 Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Sat, 1 Feb 2025 05:58:40 +0700 Subject: [PATCH] fix: fixing code and optimizing --- dto/auth_dto.go | 12 ----- dto/user_dto.go | 13 +++++ internal/handler/auth_handler.go | 28 +---------- internal/handler/user_handler.go | 30 ++++++++++++ internal/repositories/auth_repo.go | 11 +---- internal/repositories/user_repo.go | 27 +++++++++++ internal/services/auth_service.go | 76 ++---------------------------- internal/services/user_service.go | 73 ++++++++++++++++++++++++++++ middleware/api_key.go | 10 ++-- middleware/auth_middleware.go | 14 ++---- middleware/role_middleware.go | 11 +++-- presentation/auth_route.go | 2 +- presentation/user_route.go | 18 +++++++ router/setup_routes.go.go | 1 + utils/redis_caching.go | 57 ++++++++++------------ 15 files changed, 207 insertions(+), 176 deletions(-) create mode 100644 dto/user_dto.go create mode 100644 internal/handler/user_handler.go create mode 100644 internal/repositories/user_repo.go create mode 100644 internal/services/user_service.go create mode 100644 presentation/user_route.go diff --git a/dto/auth_dto.go b/dto/auth_dto.go index 2a05237..0542d95 100644 --- a/dto/auth_dto.go +++ b/dto/auth_dto.go @@ -27,18 +27,6 @@ type RegisterDTO struct { RoleID string `json:"roleId,omitempty"` } -type UserResponseDTO struct { - ID string `json:"id"` - Username string `json:"username"` - Name string `json:"name"` - Phone string `json:"phone"` - Email string `json:"email"` - EmailVerified bool `json:"emailVerified"` - RoleName string `json:"role"` - CreatedAt string `json:"createdAt"` - UpdatedAt string `json:"updatedAt"` -} - func (l *LoginDTO) Validate() (map[string][]string, bool) { errors := make(map[string][]string) diff --git a/dto/user_dto.go b/dto/user_dto.go new file mode 100644 index 0000000..783e473 --- /dev/null +++ b/dto/user_dto.go @@ -0,0 +1,13 @@ +package dto + +type UserResponseDTO struct { + ID string `json:"id"` + Username string `json:"username"` + Name string `json:"name"` + Phone string `json:"phone"` + Email string `json:"email"` + EmailVerified bool `json:"emailVerified"` + RoleName string `json:"role"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} diff --git a/internal/handler/auth_handler.go b/internal/handler/auth_handler.go index 8e0d6dd..edb98ac 100644 --- a/internal/handler/auth_handler.go +++ b/internal/handler/auth_handler.go @@ -1,14 +1,12 @@ package handler import ( - "errors" "log" "github.com/gofiber/fiber/v2" "github.com/pahmiudahgede/senggoldong/dto" "github.com/pahmiudahgede/senggoldong/internal/services" "github.com/pahmiudahgede/senggoldong/utils" - "golang.org/x/crypto/bcrypt" ) type UserHandler struct { @@ -32,36 +30,12 @@ func (h *UserHandler) Login(c *fiber.Ctx) error { user, err := h.UserService.Login(loginDTO) if err != nil { - if err.Error() == "akun dengan role tersebut belum terdaftar" { - return utils.GenericErrorResponse(c, fiber.StatusNotFound, "akun dengan role tersebut belum terdaftar") - } - if err.Error() == "password yang anda masukkan salah" { - return utils.GenericErrorResponse(c, fiber.StatusUnauthorized, "password yang anda masukkan salah") - } - if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) { - return utils.GenericErrorResponse(c, fiber.StatusUnauthorized, "password yang anda masukkan salah") - } - return utils.GenericErrorResponse(c, fiber.StatusNotFound, "akun tidak ditemukan") + return utils.GenericErrorResponse(c, fiber.StatusUnauthorized, err.Error()) } return utils.LogResponse(c, user, "Login successful") } -func (h *UserHandler) GetUserProfile(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.GenericErrorResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") - } - - user, err := h.UserService.GetUserProfile(userID) - if err != nil { - return utils.ErrorResponse(c, "User not found") - } - - return utils.LogResponse(c, user, "User profile retrieved successfully") -} - func (h *UserHandler) Register(c *fiber.Ctx) error { var registerDTO dto.RegisterDTO if err := c.BodyParser(®isterDTO); err != nil { diff --git a/internal/handler/user_handler.go b/internal/handler/user_handler.go new file mode 100644 index 0000000..cbe60d3 --- /dev/null +++ b/internal/handler/user_handler.go @@ -0,0 +1,30 @@ +package handler + +import ( + "github.com/gofiber/fiber/v2" + "github.com/pahmiudahgede/senggoldong/internal/services" + "github.com/pahmiudahgede/senggoldong/utils" +) + +type UserProfileHandler struct { + UserProfileService services.UserProfileService +} + +func NewUserProfileHandler(userProfileService services.UserProfileService) *UserProfileHandler { + return &UserProfileHandler{UserProfileService: userProfileService} +} + +func (h *UserProfileHandler) GetUserProfile(c *fiber.Ctx) error { + + userID, ok := c.Locals("userID").(string) + if !ok || userID == "" { + return utils.GenericErrorResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") + } + + userProfile, err := h.UserProfileService.GetUserProfile(userID) + if err != nil { + return utils.GenericErrorResponse(c, fiber.StatusNotFound, err.Error()) + } + + return utils.LogResponse(c, userProfile, "User profile retrieved successfully") +} diff --git a/internal/repositories/auth_repo.go b/internal/repositories/auth_repo.go index 6fbbd81..7b91c2b 100644 --- a/internal/repositories/auth_repo.go +++ b/internal/repositories/auth_repo.go @@ -11,7 +11,7 @@ type UserRepository interface { FindByUsername(username string) (*model.User, error) FindByPhoneAndRole(phone, roleID string) (*model.User, error) FindByEmailAndRole(email, roleID string) (*model.User, error) - FindByID(userID string) (*model.User, error) + Create(user *model.User) error } @@ -23,15 +23,6 @@ func NewUserRepository(db *gorm.DB) UserRepository { return &userRepository{DB: db} } -func (r *userRepository) FindByID(userID string) (*model.User, error) { - var user model.User - err := r.DB.Preload("Role").Where("id = ?", userID).First(&user).Error - if err != nil { - return nil, err - } - return &user, nil -} - func (r *userRepository) FindByIdentifierAndRole(identifier, roleID 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 diff --git a/internal/repositories/user_repo.go b/internal/repositories/user_repo.go new file mode 100644 index 0000000..f01595c --- /dev/null +++ b/internal/repositories/user_repo.go @@ -0,0 +1,27 @@ +package repositories + +import ( + "github.com/pahmiudahgede/senggoldong/model" + "gorm.io/gorm" +) + +type UserProfileRepository interface { + FindByID(userID string) (*model.User, error) +} + +type userProfileRepository struct { + DB *gorm.DB +} + +func NewUserProfileRepository(db *gorm.DB) UserProfileRepository { + return &userProfileRepository{DB: db} +} + +func (r *userProfileRepository) FindByID(userID string) (*model.User, error) { + var user model.User + err := r.DB.Preload("Role").Where("id = ?", userID).First(&user).Error + if err != nil { + return nil, err + } + return &user, nil +} diff --git a/internal/services/auth_service.go b/internal/services/auth_service.go index 0f90f3f..b0631f5 100644 --- a/internal/services/auth_service.go +++ b/internal/services/auth_service.go @@ -1,7 +1,6 @@ package services import ( - "context" "errors" "fmt" "time" @@ -17,7 +16,6 @@ import ( type UserService interface { Login(credentials dto.LoginDTO) (*dto.UserResponseWithToken, error) Register(user dto.RegisterDTO) (*model.User, error) - GetUserProfile(userID string) (*dto.UserResponseDTO, error) } type userService struct { @@ -31,6 +29,7 @@ func NewUserService(userRepo repositories.UserRepository, roleRepo repositories. } func (s *userService) Login(credentials dto.LoginDTO) (*dto.UserResponseWithToken, error) { + if credentials.RoleID == "" { return nil, errors.New("roleId is required") } @@ -49,7 +48,6 @@ func (s *userService) Login(credentials dto.LoginDTO) (*dto.UserResponseWithToke return nil, err } - ctx := context.Background() sessionKey := fmt.Sprintf("session:%s", user.ID) sessionData := map[string]interface{}{ "userID": user.ID, @@ -57,7 +55,7 @@ func (s *userService) Login(credentials dto.LoginDTO) (*dto.UserResponseWithToke "roleName": user.Role.RoleName, } - err = utils.SetJSONData(ctx, sessionKey, sessionData, time.Hour*24) + err = utils.SetJSONData(sessionKey, sessionData, time.Hour*24) if err != nil { return nil, err } @@ -91,76 +89,8 @@ func CheckPasswordHash(password, hashedPassword string) bool { return err == nil } -func (s *userService) GetUserProfile(userID string) (*dto.UserResponseDTO, error) { - ctx := context.Background() - cacheKey := "user:profile:" + userID - - if exists, _ := utils.CheckKeyExists(ctx, cacheKey); exists { - cachedUser, _ := utils.GetJSONData(ctx, cacheKey) - if cachedUser != nil { - if userDTO, ok := mapToUserResponseDTO(cachedUser); ok { - return userDTO, nil - } - } - } - - user, err := s.UserRepo.FindByID(userID) - if err != nil { - return nil, errors.New("user not found") - } - - createdAt, _ := utils.FormatDateToIndonesianFormat(user.CreatedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(user.UpdatedAt) - - userResponse := &dto.UserResponseDTO{ - ID: user.ID, - Username: user.Username, - Name: user.Name, - Phone: user.Phone, - Email: user.Email, - EmailVerified: user.EmailVerified, - RoleName: user.Role.RoleName, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - } - - err = utils.SetJSONData(ctx, cacheKey, userResponse, 5*time.Minute) - if err != nil { - return nil, errors.New("failed to cache user data") - } - - return userResponse, nil -} - -func mapToUserResponseDTO(data map[string]interface{}) (*dto.UserResponseDTO, bool) { - id, idOk := data["id"].(string) - username, usernameOk := data["username"].(string) - name, nameOk := data["name"].(string) - phone, phoneOk := data["phone"].(string) - email, emailOk := data["email"].(string) - emailVerified, emailVerifiedOk := data["emailVerified"].(bool) - roleName, roleNameOk := data["roleName"].(string) - createdAt, createdAtOk := data["createdAt"].(string) - updatedAt, updatedAtOk := data["updatedAt"].(string) - - if !(idOk && usernameOk && nameOk && phoneOk && emailOk && emailVerifiedOk && roleNameOk && createdAtOk && updatedAtOk) { - return nil, false - } - - return &dto.UserResponseDTO{ - ID: id, - Username: username, - Name: name, - Phone: phone, - Email: email, - EmailVerified: emailVerified, - RoleName: roleName, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - }, true -} - func (s *userService) Register(user dto.RegisterDTO) (*model.User, error) { + if user.Password != user.ConfirmPassword { return nil, fmt.Errorf("password and confirm password do not match") } diff --git a/internal/services/user_service.go b/internal/services/user_service.go new file mode 100644 index 0000000..214e77a --- /dev/null +++ b/internal/services/user_service.go @@ -0,0 +1,73 @@ +package services + +import ( + "encoding/json" + "errors" + "fmt" + "time" + + "github.com/pahmiudahgede/senggoldong/dto" + "github.com/pahmiudahgede/senggoldong/internal/repositories" + "github.com/pahmiudahgede/senggoldong/utils" +) + +type UserProfileService interface { + GetUserProfile(userID string) (*dto.UserResponseDTO, error) +} + +type userProfileService struct { + UserProfileRepo repositories.UserProfileRepository +} + +func NewUserProfileService(userProfileRepo repositories.UserProfileRepository) UserProfileService { + return &userProfileService{UserProfileRepo: userProfileRepo} +} + +func (s *userProfileService) GetUserProfile(userID string) (*dto.UserResponseDTO, error) { + + cacheKey := fmt.Sprintf("userProfile:%s", userID) + cachedData, err := utils.GetJSONData(cacheKey) + if err == nil && cachedData != nil { + + userResponse := &dto.UserResponseDTO{} + + if data, ok := cachedData["data"].(string); ok { + + if err := json.Unmarshal([]byte(data), userResponse); err != nil { + return nil, err + } + return userResponse, nil + } + } + + user, err := s.UserProfileRepo.FindByID(userID) + if err != nil { + return nil, errors.New("user not found") + } + + createdAt, _ := utils.FormatDateToIndonesianFormat(user.CreatedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(user.UpdatedAt) + + userResponse := &dto.UserResponseDTO{ + ID: user.ID, + Username: user.Username, + Name: user.Name, + Phone: user.Phone, + Email: user.Email, + EmailVerified: user.EmailVerified, + RoleName: user.Role.RoleName, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + } + + cacheData := map[string]interface{}{ + "data": userResponse, + } + err = utils.SetJSONData(cacheKey, cacheData, time.Hour*24) + if err != nil { + + fmt.Printf("Error caching user profile to Redis: %v\n", err) + } + + return userResponse, nil +} diff --git a/middleware/api_key.go b/middleware/api_key.go index 6a720f8..595950c 100644 --- a/middleware/api_key.go +++ b/middleware/api_key.go @@ -1,7 +1,6 @@ package middleware import ( - "log" "os" "github.com/gofiber/fiber/v2" @@ -9,15 +8,14 @@ import ( ) func APIKeyMiddleware(c *fiber.Ctx) error { - apiKey := c.Get("x-api-key") + if apiKey == "" { + return utils.GenericErrorResponse(c, fiber.StatusUnauthorized, "Unauthorized: API key is required") + } validAPIKey := os.Getenv("API_KEY") - if apiKey != validAPIKey { - log.Printf("Invalid API Key: %s", apiKey) - - return utils.GenericErrorResponse(c, fiber.StatusUnauthorized, "Unauthorized: api key yang anda masukkan tidak valid") + return utils.GenericErrorResponse(c, fiber.StatusUnauthorized, "Unauthorized: Invalid API key") } return c.Next() diff --git a/middleware/auth_middleware.go b/middleware/auth_middleware.go index 1c2f84c..6c1dbff 100644 --- a/middleware/auth_middleware.go +++ b/middleware/auth_middleware.go @@ -1,8 +1,6 @@ package middleware import ( - "context" - "log" "os" "github.com/gofiber/fiber/v2" @@ -23,7 +21,6 @@ func AuthMiddleware(c *fiber.Ctx) error { token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { return []byte(os.Getenv("SECRET_KEY")), nil }) - if err != nil || !token.Valid { return utils.GenericErrorResponse(c, fiber.StatusUnauthorized, "Unauthorized: Invalid token") } @@ -33,25 +30,20 @@ func AuthMiddleware(c *fiber.Ctx) error { return utils.GenericErrorResponse(c, fiber.StatusUnauthorized, "Unauthorized: Invalid token claims") } - userID, ok := claims["sub"].(string) - if !ok || userID == "" { - log.Println("Invalid userID format in token") + userID := claims["sub"].(string) + if userID == "" { return utils.GenericErrorResponse(c, fiber.StatusUnauthorized, "Unauthorized: Invalid user session") } - ctx := context.Background() sessionKey := "session:" + userID - sessionData, err := utils.GetJSONData(ctx, sessionKey) + sessionData, err := utils.GetJSONData(sessionKey) if err != nil || sessionData == nil { - log.Println("Session expired or invalid for userID:", userID) return utils.GenericErrorResponse(c, fiber.StatusUnauthorized, "Session expired or invalid") } roleID, roleOK := sessionData["roleID"].(string) roleName, roleNameOK := sessionData["roleName"].(string) - if !roleOK || !roleNameOK { - log.Println("Invalid session data for userID:", userID) return utils.GenericErrorResponse(c, fiber.StatusUnauthorized, "Unauthorized: Invalid session data") } diff --git a/middleware/role_middleware.go b/middleware/role_middleware.go index d3631d2..91fc152 100644 --- a/middleware/role_middleware.go +++ b/middleware/role_middleware.go @@ -1,18 +1,20 @@ package middleware import ( - "log" - "github.com/gofiber/fiber/v2" "github.com/pahmiudahgede/senggoldong/utils" ) func RoleMiddleware(allowedRoles ...string) fiber.Handler { return func(c *fiber.Ctx) error { + + if len(allowedRoles) == 0 { + return utils.GenericErrorResponse(c, fiber.StatusForbidden, "Forbidden: No roles specified") + } + roleID, ok := c.Locals("roleID").(string) if !ok || roleID == "" { - log.Println("Unauthorized access: Role not found in session") - return utils.GenericErrorResponse(c, fiber.StatusUnauthorized, "Unauthorized: Role not found in session") + return utils.GenericErrorResponse(c, fiber.StatusUnauthorized, "Unauthorized: Role not found") } for _, role := range allowedRoles { @@ -21,7 +23,6 @@ func RoleMiddleware(allowedRoles ...string) fiber.Handler { } } - log.Println("Access denied for role:", roleID) return utils.GenericErrorResponse(c, fiber.StatusForbidden, "Access Denied: You don't have permission to access this resource") } } diff --git a/presentation/auth_route.go b/presentation/auth_route.go index 0ac6233..138992e 100644 --- a/presentation/auth_route.go +++ b/presentation/auth_route.go @@ -27,5 +27,5 @@ func AuthRouter(api fiber.Router) { api.Post("/login", userHandler.Login) api.Post("/register", userHandler.Register) api.Post("/logout", middleware.AuthMiddleware, userHandler.Logout) - api.Get("/user", middleware.AuthMiddleware, userHandler.GetUserProfile) + } diff --git a/presentation/user_route.go b/presentation/user_route.go new file mode 100644 index 0000000..14deec6 --- /dev/null +++ b/presentation/user_route.go @@ -0,0 +1,18 @@ +package presentation + +import ( + "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" +) + +func UserProfileRouter(api fiber.Router) { + userProfileRepo := repositories.NewUserProfileRepository(config.DB) + userProfileService := services.NewUserProfileService(userProfileRepo) + userProfileHandler := handler.NewUserProfileHandler(userProfileService) + + api.Get("/user", middleware.AuthMiddleware, userProfileHandler.GetUserProfile) +} diff --git a/router/setup_routes.go.go b/router/setup_routes.go.go index 2d4a0aa..12e5c27 100644 --- a/router/setup_routes.go.go +++ b/router/setup_routes.go.go @@ -10,4 +10,5 @@ func SetupRoutes(app *fiber.App) { api := app.Group("/apirijikid") api.Use(middleware.APIKeyMiddleware) presentation.AuthRouter(api) + presentation.UserProfileRouter(api) } diff --git a/utils/redis_caching.go b/utils/redis_caching.go index 4e9f009..26999f4 100644 --- a/utils/redis_caching.go +++ b/utils/redis_caching.go @@ -12,30 +12,35 @@ import ( var ctx = context.Background() -func SetData(ctx context.Context, key string, value interface{}, expiration time.Duration) error { +const defaultExpiration = 1 * time.Hour + +func SetData[T any](key string, value T, expiration time.Duration) error { + if expiration == 0 { + expiration = defaultExpiration + } + jsonData, err := json.Marshal(value) if err != nil { - log.Printf("Error marshaling JSON data: %v", err) - return err + return logAndReturnError("Error marshaling data to JSON", err) } err = config.RedisClient.Set(ctx, key, jsonData, expiration).Err() if err != nil { - log.Printf("Error setting JSON data to Redis: %v", err) - return err + return logAndReturnError("Error setting data in Redis", err) } - log.Printf("JSON Data stored in Redis with key: %s", key) + log.Printf("Data stored in Redis with key: %s", key) return nil } -func GetData(ctx context.Context, key string) (string, error) { +func GetData(key string) (string, error) { val, err := config.RedisClient.Get(ctx, key).Result() if err == redis.Nil { + return "", nil } else if err != nil { - log.Printf("Error getting data from Redis: %v", err) - return "", err + + return "", logAndReturnError("Error retrieving data from Redis", err) } return val, nil } @@ -43,41 +48,26 @@ func GetData(ctx context.Context, key string) (string, error) { func DeleteData(key string) error { err := config.RedisClient.Del(ctx, key).Err() if err != nil { - log.Printf("Error deleting data from Redis: %v", err) - return err + return logAndReturnError("Error deleting data from Redis", err) } log.Printf("Data deleted from Redis with key: %s", key) return nil } -func CheckKeyExists(ctx context.Context, key string) (bool, error) { +func CheckKeyExists(key string) (bool, error) { val, err := config.RedisClient.Exists(ctx, key).Result() if err != nil { - log.Printf("Error checking if key exists in Redis: %v", err) - return false, err + return false, logAndReturnError("Error checking if key exists in Redis", err) } return val > 0, nil } -func SetJSONData(ctx context.Context, key string, value interface{}, expiration time.Duration) error { - jsonData, err := json.Marshal(value) - if err != nil { - log.Printf("Error marshaling JSON data: %v", err) - return err - } - - err = config.RedisClient.Set(ctx, key, jsonData, expiration).Err() - if err != nil { - log.Printf("Error setting JSON data to Redis: %v", err) - return err - } - - log.Printf("JSON Data stored in Redis with key: %s", key) - return nil +func SetJSONData[T any](key string, value T, expiration time.Duration) error { + return SetData(key, value, expiration) } -func GetJSONData(ctx context.Context, key string) (map[string]interface{}, error) { - val, err := GetData(ctx, key) +func GetJSONData(key string) (map[string]interface{}, error) { + val, err := GetData(key) if err != nil || val == "" { return nil, err } @@ -96,3 +86,8 @@ func DeleteSessionData(userID string) error { sessionKey := "session:" + userID return DeleteData(sessionKey) } + +func logAndReturnError(message string, err error) error { + log.Printf("%s: %v", message, err) + return err +}