diff --git a/cmd/main.go b/cmd/main.go index c645b7a..a80f884 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -4,7 +4,7 @@ import ( "github.com/gofiber/fiber/v2" "github.com/pahmiudahgede/senggoldong/config" "github.com/pahmiudahgede/senggoldong/middleware" - "github.com/pahmiudahgede/senggoldong/presentation" + "github.com/pahmiudahgede/senggoldong/router" ) func main() { @@ -12,6 +12,8 @@ func main() { app := fiber.New() app.Use(middleware.APIKeyMiddleware) - presentation.AuthRouter(app) + + router.SetupRoutes(app) + config.StartServer(app) } diff --git a/internal/handler/auth_handler.go b/internal/handler/auth_handler.go index 3749e6b..8e0d6dd 100644 --- a/internal/handler/auth_handler.go +++ b/internal/handler/auth_handler.go @@ -2,6 +2,7 @@ package handler import ( "errors" + "log" "github.com/gofiber/fiber/v2" "github.com/pahmiudahgede/senggoldong/dto" @@ -46,6 +47,21 @@ func (h *UserHandler) Login(c *fiber.Ctx) 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 { @@ -81,14 +97,13 @@ func (h *UserHandler) Register(c *fiber.Ctx) error { } func (h *UserHandler) Logout(c *fiber.Ctx) error { - - token := c.Get("Authorization") - if token == "" { - - return utils.ErrorResponse(c, "No token provided") + 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") } - err := utils.DeleteData(token) + err := utils.DeleteSessionData(userID) if err != nil { return utils.InternalServerErrorResponse(c, "Error logging out") } diff --git a/internal/repositories/auth_repo.go b/internal/repositories/auth_repo.go index b15d94f..6fbbd81 100644 --- a/internal/repositories/auth_repo.go +++ b/internal/repositories/auth_repo.go @@ -11,6 +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 } @@ -22,6 +23,15 @@ 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/services/auth_service.go b/internal/services/auth_service.go index da76f07..0f90f3f 100644 --- a/internal/services/auth_service.go +++ b/internal/services/auth_service.go @@ -1,6 +1,7 @@ package services import ( + "context" "errors" "fmt" "time" @@ -16,6 +17,7 @@ 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 { @@ -47,6 +49,7 @@ 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, @@ -54,7 +57,7 @@ func (s *userService) Login(credentials dto.LoginDTO) (*dto.UserResponseWithToke "roleName": user.Role.RoleName, } - err = utils.SetJSONData(sessionKey, sessionData, time.Hour*24) + err = utils.SetJSONData(ctx, sessionKey, sessionData, time.Hour*24) if err != nil { return nil, err } @@ -88,6 +91,75 @@ 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") @@ -139,4 +211,4 @@ func (s *userService) Register(user dto.RegisterDTO) (*model.User, error) { newUser.Role = *role return &newUser, nil -} +} \ No newline at end of file diff --git a/middleware/auth_middleware.go b/middleware/auth_middleware.go index eb07e10..1c2f84c 100644 --- a/middleware/auth_middleware.go +++ b/middleware/auth_middleware.go @@ -1,6 +1,8 @@ package middleware import ( + "context" + "log" "os" "github.com/gofiber/fiber/v2" @@ -14,6 +16,10 @@ func AuthMiddleware(c *fiber.Ctx) error { return utils.GenericErrorResponse(c, fiber.StatusUnauthorized, "Unauthorized: No token provided") } + if len(tokenString) > 7 && tokenString[:7] == "Bearer " { + tokenString = tokenString[7:] + } + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { return []byte(os.Getenv("SECRET_KEY")), nil }) @@ -23,19 +29,35 @@ func AuthMiddleware(c *fiber.Ctx) error { } claims, ok := token.Claims.(jwt.MapClaims) - if !ok { + if !ok || claims["sub"] == nil { return utils.GenericErrorResponse(c, fiber.StatusUnauthorized, "Unauthorized: Invalid token claims") } - sessionKey := "session:" + claims["sub"].(string) - sessionData, err := utils.GetJSONData(sessionKey) - if err != nil { + userID, ok := claims["sub"].(string) + if !ok || userID == "" { + log.Println("Invalid userID format in token") + return utils.GenericErrorResponse(c, fiber.StatusUnauthorized, "Unauthorized: Invalid user session") + } + + ctx := context.Background() + sessionKey := "session:" + userID + sessionData, err := utils.GetJSONData(ctx, 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") } - c.Locals("userID", sessionData["userID"]) - c.Locals("roleID", sessionData["roleID"]) - c.Locals("roleName", sessionData["roleName"]) + 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") + } + + c.Locals("userID", userID) + c.Locals("roleID", roleID) + c.Locals("roleName", roleName) return c.Next() } diff --git a/middleware/role_middleware.go b/middleware/role_middleware.go index 18c3aaf..d3631d2 100644 --- a/middleware/role_middleware.go +++ b/middleware/role_middleware.go @@ -1,14 +1,17 @@ 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 { - roleID, exists := c.Locals("roleID").(string) - if !exists || roleID == "" { + 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") } @@ -18,6 +21,7 @@ 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/model/address_model.go b/model/address_model.go new file mode 100644 index 0000000..180e431 --- /dev/null +++ b/model/address_model.go @@ -0,0 +1,18 @@ +package model + +import "time" + +type Address struct { + ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` + UserID string `gorm:"not null" json:"userId"` + User User `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"user"` + Province string `gorm:"not null" json:"province"` + District string `gorm:"not null" json:"district"` + Subdistrict string `gorm:"not null" json:"subdistrict"` + PostalCode int `gorm:"not null" json:"postalCode"` + Village string `gorm:"not null" json:"village"` + Detail string `gorm:"not null" json:"detail"` + Geography string `gorm:"not null" json:"geography"` + CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"` + UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"` +} diff --git a/presentation/auth_route.go b/presentation/auth_route.go index 687492f..0ac6233 100644 --- a/presentation/auth_route.go +++ b/presentation/auth_route.go @@ -1,6 +1,7 @@ package presentation import ( + "log" "os" "github.com/gofiber/fiber/v2" @@ -8,22 +9,23 @@ import ( "github.com/pahmiudahgede/senggoldong/internal/handler" "github.com/pahmiudahgede/senggoldong/internal/repositories" "github.com/pahmiudahgede/senggoldong/internal/services" + "github.com/pahmiudahgede/senggoldong/middleware" ) -func AuthRouter(app *fiber.App) { - api := app.Group("/apirijikid") - +func AuthRouter(api fiber.Router) { secretKey := os.Getenv("SECRET_KEY") if secretKey == "" { - panic("SECRET_KEY is not set in the environment variables") + 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", userHandler.Logout) + api.Post("/logout", middleware.AuthMiddleware, userHandler.Logout) + api.Get("/user", middleware.AuthMiddleware, userHandler.GetUserProfile) } diff --git a/router/setup_routes.go.go b/router/setup_routes.go.go new file mode 100644 index 0000000..73fe8b2 --- /dev/null +++ b/router/setup_routes.go.go @@ -0,0 +1,12 @@ +package router + +import ( + "github.com/gofiber/fiber/v2" + "github.com/pahmiudahgede/senggoldong/presentation" +) + +func SetupRoutes(app *fiber.App) { + api := app.Group("/apirijikid") + + presentation.AuthRouter(api) +} diff --git a/utils/redis_caching.go b/utils/redis_caching.go index 163b885..4e9f009 100644 --- a/utils/redis_caching.go +++ b/utils/redis_caching.go @@ -10,31 +10,38 @@ import ( "github.com/pahmiudahgede/senggoldong/config" ) -func SetData(key string, value interface{}, expiration time.Duration) error { - err := config.RedisClient.Set(context.Background(), key, value, expiration).Err() +var ctx = context.Background() + +func SetData(ctx context.Context, key string, value interface{}, expiration time.Duration) error { + jsonData, err := json.Marshal(value) if err != nil { - log.Printf("Error setting data to Redis: %v", err) + log.Printf("Error marshaling JSON data: %v", err) return err } - log.Printf("Data stored in Redis with key: %s", key) + + 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 GetData(key string) (string, error) { - val, err := config.RedisClient.Get(context.Background(), key).Result() +func GetData(ctx context.Context, key string) (string, error) { + val, err := config.RedisClient.Get(ctx, key).Result() if err == redis.Nil { - log.Printf("No data found for key: %s", key) return "", nil } else if err != nil { log.Printf("Error getting data from Redis: %v", err) return "", err } - log.Printf("Data retrieved from Redis for key: %s", key) return val, nil } func DeleteData(key string) error { - err := config.RedisClient.Del(context.Background(), key).Err() + err := config.RedisClient.Del(ctx, key).Err() if err != nil { log.Printf("Error deleting data from Redis: %v", err) return err @@ -43,18 +50,8 @@ func DeleteData(key string) error { return nil } -func SetDataWithExpire(key string, value interface{}, expiration time.Duration) error { - err := config.RedisClient.Set(context.Background(), key, value, expiration).Err() - if err != nil { - log.Printf("Error setting data with expiration to Redis: %v", err) - return err - } - log.Printf("Data stored in Redis with key: %s and expiration: %v", key, expiration) - return nil -} - -func CheckKeyExists(key string) (bool, error) { - val, err := config.RedisClient.Exists(context.Background(), key).Result() +func CheckKeyExists(ctx context.Context, 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 @@ -62,17 +59,25 @@ func CheckKeyExists(key string) (bool, error) { return val > 0, nil } -func SetJSONData(key string, value interface{}, expiration time.Duration) error { +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 } - return SetData(key, jsonData, expiration) + + 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 GetJSONData(key string) (map[string]interface{}, error) { - val, err := GetData(key) +func GetJSONData(ctx context.Context, key string) (map[string]interface{}, error) { + val, err := GetData(ctx, key) if err != nil || val == "" { return nil, err } @@ -86,3 +91,8 @@ func GetJSONData(key string) (map[string]interface{}, error) { return data, nil } + +func DeleteSessionData(userID string) error { + sessionKey := "session:" + userID + return DeleteData(sessionKey) +}