diff --git a/config/connection.go b/config/connection.go deleted file mode 100644 index aaf5522..0000000 --- a/config/connection.go +++ /dev/null @@ -1,112 +0,0 @@ -package config - -import ( - "context" - "fmt" - "log" - "os" - - "github.com/go-redis/redis/v8" - "github.com/pahmiudahgede/senggoldong/domain" - "gorm.io/driver/postgres" - "gorm.io/gorm" -) - -var ( - DB *gorm.DB - DBHost string - DBPort string - DBName string - DBUser string - DBPassword string - - APIKey string - ServerHost string - ServerPort string - - RedisClient *redis.Client - RedisHost string - RedisPort string - RedisPassword string - RedisDB int -) - -func Context() context.Context { - return context.Background() -} - -func InitConfig() { - ServerHost = os.Getenv("SERVER_HOST") - ServerPort = os.Getenv("SERVER_PORT") - DBHost = os.Getenv("DB_HOST") - DBPort = os.Getenv("DB_PORT") - DBName = os.Getenv("DB_NAME") - DBUser = os.Getenv("DB_USER") - DBPassword = os.Getenv("DB_PASSWORD") - APIKey = os.Getenv("API_KEY") - RedisHost = os.Getenv("REDIS_HOST") - RedisPort = os.Getenv("REDIS_PORT") - RedisPassword = os.Getenv("REDIS_PASSWORD") - RedisDB = 0 - - if ServerHost == "" || ServerPort == "" || DBHost == "" || DBPort == "" || DBName == "" || DBUser == "" || DBPassword == "" || APIKey == "" { - log.Fatal("Error: environment variables yang dibutuhkan tidak ada") - } -} - -func InitDatabase() { - InitConfig() - - dsn := fmt.Sprintf("host=%s port=%s user=%s dbname=%s password=%s sslmode=disable", - DBHost, DBPort, DBUser, DBName, DBPassword) - - var err error - DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{}) - if err != nil { - log.Fatal("gagal terhubung ke database: ", err) - } - - err = DB.AutoMigrate( - &domain.Point{}, - &domain.User{}, - &domain.UserRole{}, - &domain.UserPin{}, - &domain.MenuAccess{}, - &domain.PlatformHandle{}, - &domain.Address{}, - &domain.Article{}, - &domain.TrashCategory{}, - &domain.TrashDetail{}, - &domain.Banner{}, - &domain.CoverageArea{}, - &domain.CoverageDistric{}, - &domain.CoverageSubdistrict{}, - &domain.RequestPickup{}, - &domain.RequestItem{}, - &domain.Product{}, - &domain.ProductImage{}, - &domain.Store{}, - ) - if err != nil { - log.Fatal("Error: Failed to auto migrate domain:", err) - } - - fmt.Println("Koneksi ke database berhasil dan migrasi dilakukan") -} - -func InitRedis() { - InitConfig() - - RedisClient = redis.NewClient(&redis.Options{ - Addr: fmt.Sprintf("%s:%s", RedisHost, RedisPort), - Password: RedisPassword, - DB: RedisDB, - }) - - _, err := RedisClient.Ping(context.Background()).Result() - if err != nil { - log.Fatal("Gagal terhubung ke Redis:", err) - } - - fmt.Println("Koneksi ke Redis berhasil") -} diff --git a/config/database.go b/config/database.go new file mode 100644 index 0000000..9d25b23 --- /dev/null +++ b/config/database.go @@ -0,0 +1,56 @@ +package config + +import ( + "fmt" + "log" + + "github.com/pahmiudahgede/senggoldong/domain" + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +var DB *gorm.DB + +func InitDatabase() { + + InitConfig() + + dsn := fmt.Sprintf( + "host=%s port=%s user=%s dbname=%s password=%s sslmode=disable", + DBHost, DBPort, DBUser, DBName, DBPassword, + ) + + var err error + + DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{}) + if err != nil { + log.Fatalf("Error: Gagal terhubung ke database: %v", err) + } + + err = DB.AutoMigrate( + &domain.Point{}, + &domain.User{}, + &domain.UserRole{}, + &domain.UserPin{}, + &domain.MenuAccess{}, + &domain.PlatformHandle{}, + &domain.Address{}, + &domain.Article{}, + &domain.TrashCategory{}, + &domain.TrashDetail{}, + &domain.Banner{}, + &domain.CoverageArea{}, + &domain.CoverageDistric{}, + &domain.CoverageSubdistrict{}, + &domain.RequestPickup{}, + &domain.RequestItem{}, + &domain.Product{}, + &domain.ProductImage{}, + &domain.Store{}, + ) + if err != nil { + log.Fatalf("Error: Gagal melakukan migrasi schema: %v", err) + } + + log.Println("Koneksi ke database berhasil dan migrasi schema juga berhasil") +} diff --git a/config/loader.go b/config/loader.go new file mode 100644 index 0000000..7f80e34 --- /dev/null +++ b/config/loader.go @@ -0,0 +1,44 @@ +package config + +import ( + "log" + "os" +) + +var ( + DBHost string + DBPort string + DBName string + DBUser string + DBPassword string + + RedisHost string + RedisPort string + RedisPassword string + RedisDB int + + ServerHost string + ServerPort string + APIKey string +) + +func InitConfig() { + + ServerHost = os.Getenv("SERVER_HOST") + ServerPort = os.Getenv("SERVER_PORT") + DBHost = os.Getenv("DB_HOST") + DBPort = os.Getenv("DB_PORT") + DBName = os.Getenv("DB_NAME") + DBUser = os.Getenv("DB_USER") + DBPassword = os.Getenv("DB_PASSWORD") + APIKey = os.Getenv("API_KEY") + + RedisHost = os.Getenv("REDIS_HOST") + RedisPort = os.Getenv("REDIS_PORT") + RedisPassword = os.Getenv("REDIS_PASSWORD") + RedisDB = 0 + + if ServerHost == "" || ServerPort == "" || DBHost == "" || DBPort == "" || DBName == "" || DBUser == "" || DBPassword == "" || APIKey == "" { + log.Fatal("Error: Beberapa environment variables tidak ditemukan.") + } +} diff --git a/config/redis.go b/config/redis.go new file mode 100644 index 0000000..d771094 --- /dev/null +++ b/config/redis.go @@ -0,0 +1,61 @@ +package config + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/go-redis/redis/v8" +) + +var RedisClient *redis.Client + +func Context() context.Context { + return context.Background() +} + +func InitRedis() { + + InitConfig() + + RedisClient = redis.NewClient(&redis.Options{ + Addr: fmt.Sprintf("%s:%s", RedisHost, RedisPort), + Password: RedisPassword, + DB: RedisDB, + }) + + _, err := RedisClient.Ping(context.Background()).Result() + if err != nil { + log.Fatalf("Error: Gagal terhubung ke Redis: %v", err) + } + + log.Println("Koneksi ke Redis berhasil.") +} + +func GetFromCache(key string) (string, error) { + val, err := RedisClient.Get(Context(), key).Result() + if err == redis.Nil { + + return "", nil + } else if err != nil { + return "", err + } + return val, nil +} + +func SetToCache(key string, value string, ttl time.Duration) error { + err := RedisClient.Set(Context(), key, value, ttl).Err() + if err != nil { + return err + } + return nil +} + +func DeleteFromCache(key string) error { + err := RedisClient.Del(Context(), key).Err() + if err != nil { + return err + } + return nil +} diff --git a/dto/initialcoint.go b/dto/initialcoint.go index 7c2c0ef..9c31407 100644 --- a/dto/initialcoint.go +++ b/dto/initialcoint.go @@ -1,7 +1,5 @@ package dto -import "github.com/go-playground/validator/v10" - type PointResponse struct { ID string `json:"id"` CoinName string `json:"coin_name"` @@ -10,39 +8,12 @@ type PointResponse struct { UpdatedAt string `json:"updatedAt"` } -func NewPointResponse(id, coinName string, valuePerUnit float64, createdAt, updatedAt string) PointResponse { - return PointResponse{ - ID: id, - CoinName: coinName, - ValuePerUnit: valuePerUnit, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - } -} - -type PointRequest struct { +type PointCreateRequest struct { CoinName string `json:"coin_name" validate:"required"` ValuePerUnit float64 `json:"value_perunit" validate:"required,gt=0"` } -func NewPointRequest(coinName string, valuePerUnit float64) PointRequest { - return PointRequest{ - CoinName: coinName, - ValuePerUnit: valuePerUnit, - } -} - -func (p *PointRequest) Validate() error { - validate := validator.New() - return validate.Struct(p) -} - -type PointUpdateDTO struct { - CoinName string `json:"coin_name" validate:"required"` - ValuePerUnit float64 `json:"value_perunit" validate:"required,gt=0"` -} - -func (p *PointUpdateDTO) Validate() error { - validate := validator.New() - return validate.Struct(p) +type PointUpdateRequest struct { + CoinName string `json:"coin_name,omitempty"` + ValuePerUnit float64 `json:"value_perunit,omitempty"` } \ No newline at end of file diff --git a/internal/api/routes.go b/internal/api/routes.go index fc8bdda..2445cce 100644 --- a/internal/api/routes.go +++ b/internal/api/routes.go @@ -4,22 +4,30 @@ import ( "github.com/gofiber/fiber/v2" "github.com/pahmiudahgede/senggoldong/internal/controllers" "github.com/pahmiudahgede/senggoldong/internal/middleware" + "github.com/pahmiudahgede/senggoldong/internal/repositories" + "github.com/pahmiudahgede/senggoldong/internal/services" "github.com/pahmiudahgede/senggoldong/utils" ) func AppRouter(app *fiber.App) { + // # init + pointRepo := repositories.NewPointRepository() + pointService := services.NewPointService(pointRepo) + pointController := controllers.NewPointController(pointService) + // # api group domain endpoint # api := app.Group("/apirijikid") // # API Secure # api.Use(middleware.APIKeyMiddleware) + api.Use(middleware.RateLimitMiddleware) // # user initial coint # - api.Get("/user/initial-coint", controllers.GetUserInitialCoint) - api.Get("/user/initial-coint/:id", controllers.GetUserInitialCointById) - api.Post("/user/initial-coint", middleware.RoleRequired(utils.RoleAdministrator), controllers.CreatePoint) - api.Put("/user/initial-coint/:id", middleware.RoleRequired(utils.RoleAdministrator), controllers.UpdatePoint) - api.Delete("/user/initial-coint/:id", middleware.RoleRequired(utils.RoleAdministrator), controllers.DeletePoint) + api.Get("/user/initial-coint", pointController.GetAllPoints) + api.Get("/user/initial-coint/:id", pointController.GetPointByID) + api.Post("/user/initial-coint", middleware.RoleRequired(utils.RoleAdministrator), pointController.CreatePoint) + api.Put("/user/initial-coint/:id", middleware.RoleRequired(utils.RoleAdministrator), pointController.UpdatePoint) + api.Delete("/user/initial-coint/:id", middleware.RoleRequired(utils.RoleAdministrator), pointController.DeletePoint) //# coverage area # api.Get("/coverage-areas", controllers.GetCoverageAreas) diff --git a/internal/controllers/initialcoint.go b/internal/controllers/initialcoint.go index 8540f56..82f7ea4 100644 --- a/internal/controllers/initialcoint.go +++ b/internal/controllers/initialcoint.go @@ -7,183 +7,116 @@ import ( "github.com/pahmiudahgede/senggoldong/utils" ) -func GetUserInitialCoint(c *fiber.Ctx) error { - points, err := services.GetPoints() +type PointController struct { + service *services.PointService +} + +func NewPointController(service *services.PointService) *PointController { + return &PointController{service: service} +} + +func (pc *PointController) GetAllPoints(c *fiber.Ctx) error { + points, err := pc.service.GetAllPoints() if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(utils.FormatResponse( + return c.Status(fiber.StatusInternalServerError).JSON(utils.ErrorResponse( fiber.StatusInternalServerError, "Failed to fetch points", - nil, )) } - - var pointResponses []dto.PointResponse - for _, point := range points { - pointResponses = append(pointResponses, dto.PointResponse{ - ID: point.ID, - CoinName: point.CoinName, - ValuePerUnit: point.ValuePerUnit, - CreatedAt: utils.FormatDateToIndonesianFormat(point.CreatedAt), - UpdatedAt: utils.FormatDateToIndonesianFormat(point.UpdatedAt), - }) - } - - if len(pointResponses) == 0 { - return c.Status(fiber.StatusOK).JSON(utils.FormatResponse( - fiber.StatusOK, - "Points successfully displayed but no data", - nil, - )) - } - return c.Status(fiber.StatusOK).JSON(utils.FormatResponse( fiber.StatusOK, "Points fetched successfully", - pointResponses, + points, )) - } -func GetUserInitialCointById(c *fiber.Ctx) error { +func (pc *PointController) GetPointByID(c *fiber.Ctx) error { id := c.Params("id") - - point, err := services.GetPointByID(id) + point, err := pc.service.GetPointByID(id) if err != nil { - if err.Error() == "point not found" { - return c.Status(fiber.StatusNotFound).JSON(utils.FormatResponse( - fiber.StatusNotFound, - "Point not found", - nil, - )) - } - - return c.Status(fiber.StatusInternalServerError).JSON(utils.FormatResponse( - fiber.StatusInternalServerError, - "Failed to fetch point", - nil, + return c.Status(fiber.StatusNotFound).JSON(utils.ErrorResponse( + fiber.StatusNotFound, + "Point not found", )) } - - pointResponse := dto.PointResponse{ - ID: point.ID, - CoinName: point.CoinName, - ValuePerUnit: point.ValuePerUnit, - CreatedAt: utils.FormatDateToIndonesianFormat(point.CreatedAt), - UpdatedAt: utils.FormatDateToIndonesianFormat(point.UpdatedAt), - } - return c.Status(fiber.StatusOK).JSON(utils.FormatResponse( fiber.StatusOK, "Point fetched successfully", - pointResponse, + point, )) } -func CreatePoint(c *fiber.Ctx) error { - var pointInput dto.PointRequest +func (pc *PointController) CreatePoint(c *fiber.Ctx) error { + var request dto.PointCreateRequest - if err := c.BodyParser(&pointInput); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(utils.FormatResponse( + if err := c.BodyParser(&request); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(utils.ErrorResponse( fiber.StatusBadRequest, - "Invalid input data", - nil, + "Invalid request body", )) } - if err := pointInput.Validate(); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(utils.FormatResponse( - fiber.StatusBadRequest, - "Validation failed: "+err.Error(), - nil, - )) - } - - newPoint, err := services.CreatePoint(pointInput.CoinName, pointInput.ValuePerUnit) + point, err := pc.service.CreatePoint(&request) if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(utils.FormatResponse( + return c.Status(fiber.StatusInternalServerError).JSON(utils.ErrorResponse( fiber.StatusInternalServerError, - "Failed to create point", - nil, + err.Error(), )) } - pointResponse := dto.NewPointResponse( - newPoint.ID, - newPoint.CoinName, - newPoint.ValuePerUnit, - utils.FormatDateToIndonesianFormat(newPoint.CreatedAt), - utils.FormatDateToIndonesianFormat(newPoint.UpdatedAt), - ) - return c.Status(fiber.StatusCreated).JSON(utils.FormatResponse( fiber.StatusCreated, "Point created successfully", - struct { - Point dto.PointResponse `json:"point"` - }{ - Point: pointResponse, - }, + point, )) } -func UpdatePoint(c *fiber.Ctx) error { +func (pc *PointController) UpdatePoint(c *fiber.Ctx) error { id := c.Params("id") + var request dto.PointUpdateRequest - var pointInput dto.PointUpdateDTO - - if err := c.BodyParser(&pointInput); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(utils.FormatResponse( + if err := c.BodyParser(&request); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(utils.ErrorResponse( fiber.StatusBadRequest, - "Invalid input data", - nil, + "Invalid request body", )) } - if err := pointInput.Validate(); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(utils.FormatResponse( - fiber.StatusBadRequest, - "Validation failed: "+err.Error(), - nil, - )) - } - - updatedPoint, err := services.UpdatePoint(id, pointInput.CoinName, pointInput.ValuePerUnit) + point, err := pc.service.UpdatePoint(id, &request) if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(utils.FormatResponse( + if err.Error() == "point not found" { + return c.Status(fiber.StatusNotFound).JSON(utils.ErrorResponse( + fiber.StatusNotFound, + "Point not found", + )) + } + return c.Status(fiber.StatusInternalServerError).JSON(utils.ErrorResponse( fiber.StatusInternalServerError, - "Failed to update point", - nil, + err.Error(), )) } - pointResponse := dto.NewPointResponse( - updatedPoint.ID, - updatedPoint.CoinName, - updatedPoint.ValuePerUnit, - utils.FormatDateToIndonesianFormat(updatedPoint.CreatedAt), - utils.FormatDateToIndonesianFormat(updatedPoint.UpdatedAt), - ) - return c.Status(fiber.StatusOK).JSON(utils.FormatResponse( fiber.StatusOK, "Point updated successfully", - struct { - Point dto.PointResponse `json:"point"` - }{ - Point: pointResponse, - }, + point, )) } -func DeletePoint(c *fiber.Ctx) error { +func (pc *PointController) DeletePoint(c *fiber.Ctx) error { id := c.Params("id") - err := services.DeletePoint(id) + err := pc.service.DeletePoint(id) if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(utils.FormatResponse( + if err.Error() == "point not found" { + return c.Status(fiber.StatusNotFound).JSON(utils.ErrorResponse( + fiber.StatusNotFound, + "Point not found", + )) + } + return c.Status(fiber.StatusInternalServerError).JSON(utils.ErrorResponse( fiber.StatusInternalServerError, - "Failed to delete point", - nil, + err.Error(), )) } @@ -192,4 +125,4 @@ func DeletePoint(c *fiber.Ctx) error { "Point deleted successfully", nil, )) -} +} \ No newline at end of file diff --git a/internal/middleware/api_secure.go b/internal/middleware/api_secure.go index 4f50e5d..a8b848e 100644 --- a/internal/middleware/api_secure.go +++ b/internal/middleware/api_secure.go @@ -1,28 +1,54 @@ package middleware import ( + "context" + "fmt" "os" + "time" "github.com/gofiber/fiber/v2" + "github.com/pahmiudahgede/senggoldong/config" "github.com/pahmiudahgede/senggoldong/utils" ) func APIKeyMiddleware(c *fiber.Ctx) error { - apiKey := c.Get("x-api-key") - expectedAPIKey := os.Getenv("API_KEY") if apiKey != expectedAPIKey { - - response := utils.FormatResponse( + return c.Status(fiber.StatusUnauthorized).JSON(utils.FormatResponse( fiber.StatusUnauthorized, "Invalid API Key", nil, - ) - - return c.Status(fiber.StatusUnauthorized).JSON(response) + )) } return c.Next() } + +func RateLimitMiddleware(c *fiber.Ctx) error { + apiKey := c.Get("x-api-key") + if apiKey == "" { + return c.Status(fiber.StatusUnauthorized).JSON(utils.FormatResponse( + fiber.StatusUnauthorized, + "API Key is missing", + nil, + )) + } + + ctx := context.Background() + rateLimitKey := fmt.Sprintf("rate_limit:%s", apiKey) + + count, _ := config.RedisClient.Incr(ctx, rateLimitKey).Result() + if count > 100 { + return c.Status(fiber.StatusTooManyRequests).JSON(utils.FormatResponse( + fiber.StatusTooManyRequests, + "Rate limit exceeded", + nil, + )) + } + + config.RedisClient.Expire(ctx, rateLimitKey, time.Minute) + + return c.Next() +} diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go index 7c716f2..2e3fdf7 100644 --- a/internal/middleware/auth.go +++ b/internal/middleware/auth.go @@ -13,9 +13,18 @@ import ( "github.com/pahmiudahgede/senggoldong/utils" ) +func containsRole(roles []string, role string) bool { + for _, r := range roles { + if r == role { + return true + } + } + return false +} + func RoleRequired(roles ...string) fiber.Handler { return func(c *fiber.Ctx) error { - tokenString := c.Get("Authorization") + tokenString := strings.TrimPrefix(c.Get("Authorization"), "Bearer ") if tokenString == "" { return c.Status(fiber.StatusUnauthorized).JSON(utils.FormatResponse( fiber.StatusUnauthorized, @@ -24,8 +33,6 @@ func RoleRequired(roles ...string) fiber.Handler { )) } - tokenString = strings.TrimPrefix(tokenString, "Bearer ") - ctx := context.Background() cachedToken, err := config.RedisClient.Get(ctx, "auth_token:"+tokenString).Result() if err != nil || cachedToken == "" { @@ -63,7 +70,7 @@ func RoleRequired(roles ...string) fiber.Handler { if !ok || userID == "" { return c.Status(fiber.StatusUnauthorized).JSON(utils.FormatResponse( fiber.StatusUnauthorized, - "Invalid or missing user ID in token", + "Missing or invalid user ID in token", nil, )) } @@ -72,7 +79,7 @@ func RoleRequired(roles ...string) fiber.Handler { if !ok || role == "" { return c.Status(fiber.StatusUnauthorized).JSON(utils.FormatResponse( fiber.StatusUnauthorized, - "Invalid or missing role in token", + "Missing or invalid role in token", nil, )) } @@ -80,59 +87,62 @@ func RoleRequired(roles ...string) fiber.Handler { c.Locals("userID", userID) c.Locals("role", role) - for _, r := range roles { - if r == role { - return c.Next() - } + if !containsRole(roles, role) { + return c.Status(fiber.StatusForbidden).JSON(utils.FormatResponse( + fiber.StatusForbidden, + "You do not have permission to access this resource", + nil, + )) } - return c.Status(fiber.StatusForbidden).JSON(utils.FormatResponse( - fiber.StatusForbidden, - "You do not have permission to access this resource", - nil, - )) + return c.Next() } } func AuthMiddleware(c *fiber.Ctx) error { - tokenString := c.Get("Authorization") - tokenString = strings.TrimPrefix(tokenString, "Bearer ") - + tokenString := strings.TrimPrefix(c.Get("Authorization"), "Bearer ") if tokenString == "" { - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ - "message": "Missing or invalid token", - }) + return c.Status(fiber.StatusUnauthorized).JSON(utils.FormatResponse( + fiber.StatusUnauthorized, + "Missing or invalid token", + nil, + )) } ctx := context.Background() cachedToken, err := config.RedisClient.Get(ctx, "auth_token:"+tokenString).Result() if err != nil || cachedToken == "" { - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ - "message": "Invalid or expired token", - }) + return c.Status(fiber.StatusUnauthorized).JSON(utils.FormatResponse( + fiber.StatusUnauthorized, + "Invalid or expired token", + nil, + )) } token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { return []byte(os.Getenv("API_KEY")), nil }) - if err != nil || !token.Valid { - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ - "message": "Invalid or expired token", - }) + return c.Status(fiber.StatusUnauthorized).JSON(utils.FormatResponse( + fiber.StatusUnauthorized, + "Invalid or expired token", + nil, + )) } claims, ok := token.Claims.(jwt.MapClaims) if !ok { - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ - "message": "Invalid token claims", - }) + return c.Status(fiber.StatusUnauthorized).JSON(utils.FormatResponse( + fiber.StatusUnauthorized, + "Invalid token claims", + nil, + )) } userID := claims["sub"].(string) c.Locals("userID", userID) - config.RedisClient.Expire(ctx, "auth_token:"+tokenString, time.Hour*24).Err() + config.RedisClient.Expire(ctx, "auth_token:"+tokenString, time.Hour*24) return c.Next() } \ No newline at end of file diff --git a/internal/repositories/initialcoint.go b/internal/repositories/initialcoint.go index cbf308a..ab0b0c6 100644 --- a/internal/repositories/initialcoint.go +++ b/internal/repositories/initialcoint.go @@ -1,44 +1,44 @@ package repositories import ( + "errors" + "github.com/pahmiudahgede/senggoldong/config" "github.com/pahmiudahgede/senggoldong/domain" ) -func GetPoints() ([]domain.Point, error) { +type PointRepository struct{} + +func NewPointRepository() *PointRepository { + return &PointRepository{} +} + +func (r *PointRepository) GetAll() ([]domain.Point, error) { var points []domain.Point - if err := config.DB.Find(&points).Error; err != nil { - return nil, err + err := config.DB.Find(&points).Error + if err != nil { + return nil, errors.New("failed to fetch points from database") } return points, nil } -func GetPointByID(id string) (domain.Point, error) { +func (r *PointRepository) GetByID(id string) (*domain.Point, error) { var point domain.Point - if err := config.DB.Where("id = ?", id).First(&point).Error; err != nil { - return point, err + err := config.DB.First(&point, "id = ?", id).Error + if err != nil { + return nil, errors.New("point not found") } - return point, nil + return &point, nil } -func CreatePoint(point *domain.Point) error { - - if err := config.DB.Create(point).Error; err != nil { - return err - } - return nil +func (r *PointRepository) Create(point *domain.Point) error { + return config.DB.Create(point).Error } -func UpdatePoint(point *domain.Point) error { - if err := config.DB.Save(point).Error; err != nil { - return err - } - return nil +func (r *PointRepository) Update(point *domain.Point) error { + return config.DB.Save(point).Error } -func DeletePoint(id string) error { - if err := config.DB.Where("id = ?", id).Delete(&domain.Point{}).Error; err != nil { - return err - } - return nil +func (r *PointRepository) Delete(point *domain.Point) error { + return config.DB.Delete(point).Error } \ No newline at end of file diff --git a/internal/services/initialcoint.go b/internal/services/initialcoint.go index ccfe59e..f03881c 100644 --- a/internal/services/initialcoint.go +++ b/internal/services/initialcoint.go @@ -1,65 +1,170 @@ package services import ( + "encoding/json" "errors" + "time" + "github.com/pahmiudahgede/senggoldong/config" "github.com/pahmiudahgede/senggoldong/domain" + "github.com/pahmiudahgede/senggoldong/dto" "github.com/pahmiudahgede/senggoldong/internal/repositories" + "github.com/pahmiudahgede/senggoldong/utils" ) -func GetPoints() ([]domain.Point, error) { - return repositories.GetPoints() +type PointService struct { + repo *repositories.PointRepository } -func GetPointByID(id string) (domain.Point, error) { - point, err := repositories.GetPointByID(id) +func NewPointService(repo *repositories.PointRepository) *PointService { + return &PointService{repo: repo} +} + +func (s *PointService) GetAllPoints() ([]dto.PointResponse, error) { + ctx := config.Context() + + cacheKey := "points:all" + cachedData, err := config.RedisClient.Get(ctx, cacheKey).Result() + if err == nil && cachedData != "" { + var cachedPoints []dto.PointResponse + if err := json.Unmarshal([]byte(cachedData), &cachedPoints); err == nil { + return cachedPoints, nil + } + } + + points, err := s.repo.GetAll() if err != nil { - return domain.Point{}, errors.New("point not found") + return nil, err } - return point, nil + + var result []dto.PointResponse + for _, point := range points { + result = append(result, dto.PointResponse{ + ID: point.ID, + CoinName: point.CoinName, + ValuePerUnit: point.ValuePerUnit, + CreatedAt: utils.FormatDateToIndonesianFormat(point.CreatedAt), + UpdatedAt: utils.FormatDateToIndonesianFormat(point.UpdatedAt), + }) + } + + cacheData, _ := json.Marshal(result) + config.RedisClient.Set(ctx, cacheKey, cacheData, time.Minute*5) + + return result, nil } -func CreatePoint(coinName string, valuePerUnit float64) (domain.Point, error) { +func (s *PointService) GetPointByID(id string) (*dto.PointResponse, error) { + ctx := config.Context() - newPoint := domain.Point{ - CoinName: coinName, - ValuePerUnit: valuePerUnit, + cacheKey := "points:" + id + cachedData, err := config.RedisClient.Get(ctx, cacheKey).Result() + if err == nil && cachedData != "" { + var cachedPoint dto.PointResponse + if err := json.Unmarshal([]byte(cachedData), &cachedPoint); err == nil { + return &cachedPoint, nil + } } - if err := repositories.CreatePoint(&newPoint); err != nil { - return domain.Point{}, err - } - - return newPoint, nil -} - -func UpdatePoint(id, coinName string, valuePerUnit float64) (domain.Point, error) { - - point, err := repositories.GetPointByID(id) + point, err := s.repo.GetByID(id) if err != nil { - return domain.Point{}, errors.New("point not found") + return nil, err } - point.CoinName = coinName - point.ValuePerUnit = valuePerUnit - - if err := repositories.UpdatePoint(&point); err != nil { - return domain.Point{}, err + result := &dto.PointResponse{ + ID: point.ID, + CoinName: point.CoinName, + ValuePerUnit: point.ValuePerUnit, + CreatedAt: utils.FormatDateToIndonesianFormat(point.CreatedAt), + UpdatedAt: utils.FormatDateToIndonesianFormat(point.UpdatedAt), } - return point, nil + cacheData, _ := json.Marshal(result) + config.RedisClient.Set(ctx, cacheKey, cacheData, time.Minute*5) + + return result, nil } -func DeletePoint(id string) error { +func (s *PointService) CreatePoint(request *dto.PointCreateRequest) (*dto.PointResponse, error) { - _, err := repositories.GetPointByID(id) + if request.CoinName == "" || request.ValuePerUnit <= 0 { + return nil, errors.New("invalid input data") + } + + newPoint := &domain.Point{ + CoinName: request.CoinName, + ValuePerUnit: request.ValuePerUnit, + } + + err := s.repo.Create(newPoint) + if err != nil { + return nil, err + } + + ctx := config.Context() + config.RedisClient.Del(ctx, "points:all") + + response := &dto.PointResponse{ + ID: newPoint.ID, + CoinName: newPoint.CoinName, + ValuePerUnit: newPoint.ValuePerUnit, + CreatedAt: utils.FormatDateToIndonesianFormat(newPoint.CreatedAt), + UpdatedAt: utils.FormatDateToIndonesianFormat(newPoint.UpdatedAt), + } + + return response, nil +} + +func (s *PointService) UpdatePoint(id string, request *dto.PointUpdateRequest) (*dto.PointResponse, error) { + + point, err := s.repo.GetByID(id) + if err != nil { + return nil, errors.New("point not found") + } + + if request.CoinName != "" { + point.CoinName = request.CoinName + } + if request.ValuePerUnit > 0 { + point.ValuePerUnit = request.ValuePerUnit + } + point.UpdatedAt = time.Now() + + err = s.repo.Update(point) + if err != nil { + return nil, errors.New("failed to update point") + } + + ctx := config.Context() + config.RedisClient.Del(ctx, "points:all") + config.RedisClient.Del(ctx, "points:"+id) + + response := &dto.PointResponse{ + ID: point.ID, + CoinName: point.CoinName, + ValuePerUnit: point.ValuePerUnit, + CreatedAt: utils.FormatDateToIndonesianFormat(point.CreatedAt), + UpdatedAt: utils.FormatDateToIndonesianFormat(point.UpdatedAt), + } + + return response, nil +} + +func (s *PointService) DeletePoint(id string) error { + + point, err := s.repo.GetByID(id) if err != nil { return errors.New("point not found") } - if err := repositories.DeletePoint(id); err != nil { - return err + err = s.repo.Delete(point) + if err != nil { + return errors.New("failed to delete point") } + ctx := config.Context() + config.RedisClient.Del(ctx, "points:all") + config.RedisClient.Del(ctx, "points:"+id) + return nil } diff --git a/utils/response.go b/utils/response.go index 28303c1..80eb5e5 100644 --- a/utils/response.go +++ b/utils/response.go @@ -7,7 +7,7 @@ type Meta struct { type ApiResponse struct { Meta Meta `json:"meta"` - Data interface{} `json:"data"` + Data interface{} `json:"data,omitempty"` } func FormatResponse(statusCode int, message string, data interface{}) ApiResponse { @@ -19,3 +19,12 @@ func FormatResponse(statusCode int, message string, data interface{}) ApiRespons Data: data, } } + +func ErrorResponse(statusCode int, message string) ApiResponse { + return ApiResponse{ + Meta: Meta{ + StatusCode: statusCode, + Message: message, + }, + } +} \ No newline at end of file