fix: redis caching and setup route management

This commit is contained in:
pahmiudahgede 2025-01-29 21:36:42 +07:00
parent 16bfb40f95
commit a824de7ba0
10 changed files with 217 additions and 50 deletions

View File

@ -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)
}

View File

@ -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(&registerDTO); 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")
}

View File

@ -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

View File

@ -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")

View File

@ -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()
}

View File

@ -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")
}
}

18
model/address_model.go Normal file
View File

@ -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"`
}

View File

@ -1,6 +1,7 @@
package presentation
import (
"log"
"os"
"github.com/gofiber/fiber/v2"
@ -8,14 +9,14 @@ 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)
@ -25,5 +26,6 @@ func AuthRouter(app *fiber.App) {
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)
}

12
router/setup_routes.go.go Normal file
View File

@ -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)
}

View File

@ -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
}
func GetJSONData(key string) (map[string]interface{}, error) {
val, err := GetData(key)
log.Printf("JSON Data stored in Redis with key: %s", key)
return nil
}
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)
}