diff --git a/dto/auth_dto.go b/dto/auth_dto.go index 0542d95..5a7e2ea 100644 --- a/dto/auth_dto.go +++ b/dto/auth_dto.go @@ -13,7 +13,7 @@ type LoginDTO struct { type UserResponseWithToken struct { UserID string `json:"user_id"` - RoleName string `json:"loginas"` + RoleName string `json:"role_name"` Token string `json:"token"` } @@ -49,39 +49,23 @@ func (l *LoginDTO) Validate() (map[string][]string, bool) { func (r *RegisterDTO) Validate() (map[string][]string, bool) { errors := make(map[string][]string) - if strings.TrimSpace(r.Username) == "" { - errors["username"] = append(errors["username"], "Username is required") - } - if strings.TrimSpace(r.Name) == "" { - errors["name"] = append(errors["name"], "Name is required") - } + r.validateRequiredFields(errors) - if strings.TrimSpace(r.Phone) == "" { - errors["phone"] = append(errors["phone"], "Phone number is required") - } else if !IsValidPhoneNumber(r.Phone) { + if r.Phone != "" && !IsValidPhoneNumber(r.Phone) { errors["phone"] = append(errors["phone"], "Invalid phone number format. Use +62 followed by 9-13 digits") } - if strings.TrimSpace(r.Email) == "" { - errors["email"] = append(errors["email"], "Email is required") - } else if !IsValidEmail(r.Email) { + if r.Email != "" && !IsValidEmail(r.Email) { errors["email"] = append(errors["email"], "Invalid email format") } - if strings.TrimSpace(r.Password) == "" { - errors["password"] = append(errors["password"], "Password is required") - } else if !IsValidPassword(r.Password) { + if r.Password != "" && !IsValidPassword(r.Password) { errors["password"] = append(errors["password"], "Password must be at least 8 characters long and contain at least one number") } - if strings.TrimSpace(r.ConfirmPassword) == "" { - errors["confirm_password"] = append(errors["confirm_password"], "Confirm password is required") - } else if r.Password != r.ConfirmPassword { + if r.ConfirmPassword != "" && r.Password != r.ConfirmPassword { errors["confirm_password"] = append(errors["confirm_password"], "Password and confirm password do not match") } - if strings.TrimSpace(r.RoleID) == "" { - errors["roleId"] = append(errors["roleId"], "RoleID is required") - } if len(errors) > 0 { return errors, false @@ -90,6 +74,31 @@ func (r *RegisterDTO) Validate() (map[string][]string, bool) { return nil, true } +func (r *RegisterDTO) validateRequiredFields(errors map[string][]string) { + + if strings.TrimSpace(r.Username) == "" { + errors["username"] = append(errors["username"], "Username is required") + } + if strings.TrimSpace(r.Name) == "" { + errors["name"] = append(errors["name"], "Name is required") + } + if strings.TrimSpace(r.Phone) == "" { + errors["phone"] = append(errors["phone"], "Phone number is required") + } + if strings.TrimSpace(r.Email) == "" { + errors["email"] = append(errors["email"], "Email is required") + } + if strings.TrimSpace(r.Password) == "" { + errors["password"] = append(errors["password"], "Password is required") + } + if strings.TrimSpace(r.ConfirmPassword) == "" { + errors["confirm_password"] = append(errors["confirm_password"], "Confirm password is required") + } + if strings.TrimSpace(r.RoleID) == "" { + errors["roleId"] = append(errors["roleId"], "RoleID is required") + } +} + func IsValidPhoneNumber(phone string) bool { re := regexp.MustCompile(`^\+62\d{9,13}$`) diff --git a/internal/handler/article_handler.go b/internal/handler/article_handler.go index 02c892c..1d4ad0d 100644 --- a/internal/handler/article_handler.go +++ b/internal/handler/article_handler.go @@ -18,8 +18,8 @@ func NewArticleHandler(articleService services.ArticleService) *ArticleHandler { } func (h *ArticleHandler) CreateArticle(c *fiber.Ctx) error { - var request dto.RequestArticleDTO + if err := c.BodyParser(&request); err != nil { return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}}) } @@ -45,29 +45,37 @@ func (h *ArticleHandler) CreateArticle(c *fiber.Ctx) error { func (h *ArticleHandler) GetAllArticles(c *fiber.Ctx) error { page, err := strconv.Atoi(c.Query("page", "0")) - if err != nil { + if err != nil || page < 1 { page = 0 } + limit, err := strconv.Atoi(c.Query("limit", "0")) - if err != nil { + if err != nil || limit < 1 { limit = 0 } - article, totalArticle, err := h.ArticleService.GetAllArticles(page, limit) + var articles []dto.ArticleResponseDTO + var totalArticles int + + if page == 0 && limit == 0 { + + articles, totalArticles, err = h.ArticleService.GetAllArticles(0, 0) + if err != nil { + return utils.GenericErrorResponse(c, fiber.StatusInternalServerError, "Failed to fetch articles") + } + + return utils.NonPaginatedResponse(c, articles, totalArticles, "Articles fetched successfully") + } + + articles, totalArticles, err = h.ArticleService.GetAllArticles(page, limit) if err != nil { - return utils.GenericErrorResponse(c, fiber.StatusInternalServerError, "Failed to fetch article") + return utils.GenericErrorResponse(c, fiber.StatusInternalServerError, "Failed to fetch articles") } - if page > 0 && limit > 0 { - return utils.PaginatedResponse(c, article, page, limit, totalArticle, "Article fetched successfully") - } - - return utils.NonPaginatedResponse(c, article, totalArticle, "Article fetched successfully") - + return utils.PaginatedResponse(c, articles, page, limit, totalArticles, "Articles fetched successfully") } func (h *ArticleHandler) GetArticleByID(c *fiber.Ctx) error { - id := c.Params("article_id") if id == "" { return utils.GenericErrorResponse(c, fiber.StatusBadRequest, "Article ID is required") @@ -80,3 +88,33 @@ func (h *ArticleHandler) GetArticleByID(c *fiber.Ctx) error { return utils.SuccessResponse(c, article, "Article fetched successfully") } + +func (h *ArticleHandler) UpdateArticle(c *fiber.Ctx) error { + id := c.Params("article_id") + if id == "" { + return utils.GenericErrorResponse(c, fiber.StatusBadRequest, "Article ID is required") + } + + var request dto.RequestArticleDTO + + if err := c.BodyParser(&request); err != nil { + return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}}) + } + + errors, valid := request.Validate() + if !valid { + return utils.ValidationErrorResponse(c, errors) + } + + coverImage, err := c.FormFile("coverImage") + if err != nil && err.Error() != "no such file" { + return utils.GenericErrorResponse(c, fiber.StatusBadRequest, "Cover image is required") + } + + articleResponse, err := h.ArticleService.UpdateArticle(id, request, coverImage) + if err != nil { + return utils.GenericErrorResponse(c, fiber.StatusInternalServerError, err.Error()) + } + + return utils.SuccessResponse(c, articleResponse, "Article updated successfully") +} diff --git a/internal/handler/auth_handler.go b/internal/handler/auth_handler.go index 082fc29..e886943 100644 --- a/internal/handler/auth_handler.go +++ b/internal/handler/auth_handler.go @@ -37,9 +37,10 @@ func (h *UserHandler) Login(c *fiber.Ctx) error { } func (h *UserHandler) Register(c *fiber.Ctx) error { + var registerDTO dto.RegisterDTO if err := c.BodyParser(®isterDTO); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}}) + return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid request body"}}) } errors, valid := registerDTO.Validate() @@ -47,26 +48,11 @@ func (h *UserHandler) Register(c *fiber.Ctx) error { return utils.ValidationErrorResponse(c, errors) } - user, err := h.UserService.Register(registerDTO) + userResponse, err := h.UserService.Register(registerDTO) if err != nil { return utils.GenericErrorResponse(c, fiber.StatusConflict, err.Error()) } - 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, - } - return utils.CreateResponse(c, userResponse, "Registration successful") } @@ -82,5 +68,5 @@ func (h *UserHandler) Logout(c *fiber.Ctx) error { return utils.InternalServerErrorResponse(c, "Error logging out") } - return utils.NonPaginatedResponse(c, nil, 0, "Logout successful") + return utils.SuccessResponse(c, nil, "Logout successful") } diff --git a/internal/repositories/article_repo.go b/internal/repositories/article_repo.go index 87f9c20..493a136 100644 --- a/internal/repositories/article_repo.go +++ b/internal/repositories/article_repo.go @@ -9,6 +9,7 @@ type ArticleRepository interface { CreateArticle(article *model.Article) error FindArticleByID(id string) (*model.Article, error) FindAllArticles(page, limit int) ([]model.Article, int, error) + UpdateArticle(id string, article *model.Article) error } type articleRepository struct { @@ -20,11 +21,7 @@ func NewArticleRepository(db *gorm.DB) ArticleRepository { } func (r *articleRepository) CreateArticle(article *model.Article) error { - err := r.DB.Create(article).Error - if err != nil { - return err - } - return nil + return r.DB.Create(article).Error } func (r *articleRepository) FindArticleByID(id string) (*model.Article, error) { @@ -51,6 +48,7 @@ func (r *articleRepository) FindAllArticles(page, limit int) ([]model.Article, i return nil, 0, err } } else { + err := r.DB.Find(&articles).Error if err != nil { return nil, 0, err @@ -59,3 +57,7 @@ func (r *articleRepository) FindAllArticles(page, limit int) ([]model.Article, i return articles, int(total), nil } + +func (r *articleRepository) UpdateArticle(id string, article *model.Article) error { + return r.DB.Save(article).Error +} diff --git a/internal/repositories/auth_repo.go b/internal/repositories/auth_repo.go index 7b91c2b..2b0622f 100644 --- a/internal/repositories/auth_repo.go +++ b/internal/repositories/auth_repo.go @@ -1,6 +1,8 @@ package repositories import ( + "fmt" + "github.com/pahmiudahgede/senggoldong/model" "gorm.io/gorm" ) @@ -29,6 +31,9 @@ func (r *userRepository) FindByIdentifierAndRole(identifier, roleID string) (*mo if err != nil { return nil, err } + if user.Role == nil { + return nil, fmt.Errorf("role not found for this user") + } return &user, nil } diff --git a/internal/services/article_service.go b/internal/services/article_service.go index 01814a4..702b290 100644 --- a/internal/services/article_service.go +++ b/internal/services/article_service.go @@ -18,6 +18,7 @@ type ArticleService interface { CreateArticle(request dto.RequestArticleDTO, coverImage *multipart.FileHeader) (*dto.ArticleResponseDTO, error) GetAllArticles(page, limit int) ([]dto.ArticleResponseDTO, int, error) GetArticleByID(id string) (*dto.ArticleResponseDTO, error) + UpdateArticle(id string, request dto.RequestArticleDTO, coverImage *multipart.FileHeader) (*dto.ArticleResponseDTO, error) } type articleService struct { @@ -31,11 +32,8 @@ func NewArticleService(articleRepo repositories.ArticleRepository) ArticleServic func (s *articleService) CreateArticle(request dto.RequestArticleDTO, coverImage *multipart.FileHeader) (*dto.ArticleResponseDTO, error) { coverImageDir := "./public/uploads/articles" - if _, err := os.Stat(coverImageDir); os.IsNotExist(err) { - err := os.MkdirAll(coverImageDir, os.ModePerm) - if err != nil { - return nil, fmt.Errorf("failed to create directory for cover image: %v", err) - } + if err := os.MkdirAll(coverImageDir, os.ModePerm); err != nil { + return nil, fmt.Errorf("failed to create directory for cover image: %v", err) } extension := filepath.Ext(coverImage.Filename) @@ -58,8 +56,7 @@ func (s *articleService) CreateArticle(request dto.RequestArticleDTO, coverImage } defer dst.Close() - _, err = dst.ReadFrom(src) - if err != nil { + if _, err := dst.ReadFrom(src); err != nil { return nil, fmt.Errorf("failed to save cover image: %v", err) } @@ -71,8 +68,7 @@ func (s *articleService) CreateArticle(request dto.RequestArticleDTO, coverImage Content: request.Content, } - err = s.ArticleRepo.CreateArticle(&article) - if err != nil { + if err := s.ArticleRepo.CreateArticle(&article); err != nil { return nil, fmt.Errorf("failed to create article: %v", err) } @@ -94,11 +90,49 @@ func (s *articleService) CreateArticle(request dto.RequestArticleDTO, coverImage cacheData := map[string]interface{}{ "data": articleResponseDTO, } - err = utils.SetJSONData(cacheKey, cacheData, time.Hour*24) - if err != nil { + + if err := utils.SetJSONData(cacheKey, cacheData, time.Hour*24); err != nil { + fmt.Printf("Error caching article to Redis: %v\n", err) } + articlesCacheKey := "articles:all" + if err := utils.DeleteData(articlesCacheKey); err != nil { + + fmt.Printf("Error deleting articles cache: %v\n", err) + } + + articles, _, err := s.ArticleRepo.FindAllArticles(0, 0) + if err == nil { + var articleDTOs []dto.ArticleResponseDTO + for _, a := range articles { + createdAt, _ := utils.FormatDateToIndonesianFormat(a.PublishedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(a.UpdatedAt) + + articleDTOs = append(articleDTOs, dto.ArticleResponseDTO{ + ID: a.ID, + Title: a.Title, + CoverImage: a.CoverImage, + Author: a.Author, + Heading: a.Heading, + Content: a.Content, + PublishedAt: createdAt, + UpdatedAt: updatedAt, + }) + } + + cacheData = map[string]interface{}{ + "data": articleDTOs, + } + if err := utils.SetJSONData(articlesCacheKey, cacheData, time.Hour*24); err != nil { + + fmt.Printf("Error caching all articles to Redis: %v\n", err) + } + } else { + + fmt.Printf("Error fetching all articles: %v\n", err) + } + return articleResponseDTO, nil } @@ -109,10 +143,12 @@ func (s *articleService) GetAllArticles(page, limit int) ([]dto.ArticleResponseD cachedData, err := utils.GetJSONData(cacheKey) if err == nil && cachedData != nil { var articles []dto.ArticleResponseDTO + if data, ok := cachedData["data"].([]interface{}); ok { for _, item := range data { articleData, ok := item.(map[string]interface{}) if ok { + articles = append(articles, dto.ArticleResponseDTO{ ID: articleData["article_id"].(string), Title: articleData["title"].(string), @@ -125,7 +161,12 @@ func (s *articleService) GetAllArticles(page, limit int) ([]dto.ArticleResponseD }) } } - return articles, len(articles), nil + + total, ok := cachedData["total"].(float64) + if !ok { + return nil, 0, fmt.Errorf("invalid total count in cache") + } + return articles, int(total), nil } } @@ -152,7 +193,8 @@ func (s *articleService) GetAllArticles(page, limit int) ([]dto.ArticleResponseD } cacheData := map[string]interface{}{ - "data": articleDTOs, + "data": articleDTOs, + "total": total, } err = utils.SetJSONData(cacheKey, cacheData, time.Hour*24) if err != nil { @@ -169,6 +211,7 @@ func (s *articleService) GetArticleByID(id string) (*dto.ArticleResponseDTO, err if err == nil && cachedData != nil { articleData, ok := cachedData["data"].(map[string]interface{}) if ok { + article := dto.ArticleResponseDTO{ ID: articleData["article_id"].(string), Title: articleData["title"].(string), @@ -207,8 +250,135 @@ func (s *articleService) GetArticleByID(id string) (*dto.ArticleResponseDTO, err } err = utils.SetJSONData(cacheKey, cacheData, time.Hour*24) if err != nil { + fmt.Printf("Error caching article to Redis: %v\n", err) } return articleResponseDTO, nil } + +func (s *articleService) UpdateArticle(id string, request dto.RequestArticleDTO, coverImage *multipart.FileHeader) (*dto.ArticleResponseDTO, error) { + article, err := s.ArticleRepo.FindArticleByID(id) + if err != nil { + return nil, fmt.Errorf("article not found: %v", err) + } + + article.Title = request.Title + article.Heading = request.Heading + article.Content = request.Content + article.Author = request.Author + + var coverImagePath string + if coverImage != nil { + coverImageDir := "./public/uploads/articles" + if _, err := os.Stat(coverImageDir); os.IsNotExist(err) { + err := os.MkdirAll(coverImageDir, os.ModePerm) + if err != nil { + return nil, fmt.Errorf("failed to create directory for cover image: %v", err) + } + } + + extension := filepath.Ext(coverImage.Filename) + if extension != ".jpg" && extension != ".jpeg" && extension != ".png" { + return nil, fmt.Errorf("invalid file type, only .jpg, .jpeg, and .png are allowed") + } + + coverImageFileName := fmt.Sprintf("%s_cover%s", uuid.New().String(), extension) + coverImagePath = filepath.Join(coverImageDir, coverImageFileName) + + src, err := coverImage.Open() + if err != nil { + return nil, fmt.Errorf("failed to open uploaded file: %v", err) + } + defer src.Close() + + dst, err := os.Create(coverImagePath) + if err != nil { + return nil, fmt.Errorf("failed to create cover image file: %v", err) + } + defer dst.Close() + + _, err = dst.ReadFrom(src) + if err != nil { + return nil, fmt.Errorf("failed to save cover image: %v", err) + } + + article.CoverImage = coverImagePath + } + + err = s.ArticleRepo.UpdateArticle(id, article) + if err != nil { + return nil, fmt.Errorf("failed to update article: %v", err) + } + + updatedArticle, err := s.ArticleRepo.FindArticleByID(id) + if err != nil { + return nil, fmt.Errorf("failed to fetch updated article: %v", err) + } + + createdAt, _ := utils.FormatDateToIndonesianFormat(updatedArticle.PublishedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(updatedArticle.UpdatedAt) + + articleResponseDTO := &dto.ArticleResponseDTO{ + ID: updatedArticle.ID, + Title: updatedArticle.Title, + CoverImage: updatedArticle.CoverImage, + Author: updatedArticle.Author, + Heading: updatedArticle.Heading, + Content: updatedArticle.Content, + PublishedAt: createdAt, + UpdatedAt: updatedAt, + } + + articleCacheKey := fmt.Sprintf("article:%s", updatedArticle.ID) + err = utils.DeleteData(articleCacheKey) + if err != nil { + fmt.Printf("Error deleting old cache for article: %v\n", err) + } + + cacheData := map[string]interface{}{ + "data": articleResponseDTO, + } + err = utils.SetJSONData(articleCacheKey, cacheData, time.Hour*24) + if err != nil { + fmt.Printf("Error caching updated article to Redis: %v\n", err) + } + + articlesCacheKey := "articles:all" + err = utils.DeleteData(articlesCacheKey) + if err != nil { + fmt.Printf("Error deleting articles cache: %v\n", err) + } + + articles, _, err := s.ArticleRepo.FindAllArticles(0, 0) + if err != nil { + fmt.Printf("Error fetching all articles: %v\n", err) + } else { + var articleDTOs []dto.ArticleResponseDTO + for _, a := range articles { + createdAt, _ := utils.FormatDateToIndonesianFormat(a.PublishedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(a.UpdatedAt) + + articleDTOs = append(articleDTOs, dto.ArticleResponseDTO{ + ID: a.ID, + Title: a.Title, + CoverImage: a.CoverImage, + Author: a.Author, + Heading: a.Heading, + Content: a.Content, + PublishedAt: createdAt, + UpdatedAt: updatedAt, + }) + } + + cacheData = map[string]interface{}{ + "data": articleDTOs, + } + err = utils.SetJSONData(articlesCacheKey, cacheData, time.Hour*24) + if err != nil { + fmt.Printf("Error caching updated articles to Redis: %v\n", err) + } + } + + return articleResponseDTO, nil +} diff --git a/internal/services/auth_service.go b/internal/services/auth_service.go index b0631f5..134803e 100644 --- a/internal/services/auth_service.go +++ b/internal/services/auth_service.go @@ -13,9 +13,22 @@ import ( "golang.org/x/crypto/bcrypt" ) +const ( + ErrUsernameTaken = "username is already taken" + ErrPhoneTaken = "phone number is already used for this role" + ErrEmailTaken = "email is already used for this role" + ErrInvalidRoleID = "invalid roleId" + ErrPasswordMismatch = "password and confirm password do not match" + ErrRoleIDRequired = "roleId is required" + ErrFailedToHashPassword = "failed to hash password" + ErrFailedToCreateUser = "failed to create user" + ErrIncorrectPassword = "incorrect password" + ErrAccountNotFound = "account not found" +) + type UserService interface { Login(credentials dto.LoginDTO) (*dto.UserResponseWithToken, error) - Register(user dto.RegisterDTO) (*model.User, error) + Register(user dto.RegisterDTO) (*dto.UserResponseDTO, error) } type userService struct { @@ -29,18 +42,17 @@ 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") + return nil, errors.New(ErrRoleIDRequired) } user, err := s.UserRepo.FindByIdentifierAndRole(credentials.Identifier, credentials.RoleID) if err != nil { - return nil, errors.New("akun dengan role tersebut belum terdaftar") + return nil, errors.New(ErrAccountNotFound) } if !CheckPasswordHash(credentials.Password, user.Password) { - return nil, errors.New("password yang anda masukkan salah") + return nil, errors.New(ErrIncorrectPassword) } token, err := s.generateJWT(user) @@ -89,39 +101,36 @@ func CheckPasswordHash(password, hashedPassword string) bool { return err == nil } -func (s *userService) Register(user dto.RegisterDTO) (*model.User, error) { +func (s *userService) Register(user dto.RegisterDTO) (*dto.UserResponseDTO, error) { if user.Password != user.ConfirmPassword { - return nil, fmt.Errorf("password and confirm password do not match") + return nil, fmt.Errorf("%s: %v", ErrPasswordMismatch, nil) } if user.RoleID == "" { - return nil, fmt.Errorf("roleId is required") + return nil, fmt.Errorf("%s: %v", ErrRoleIDRequired, nil) } role, err := s.RoleRepo.FindByID(user.RoleID) if err != nil { - return nil, fmt.Errorf("invalid roleId") + return nil, fmt.Errorf("%s: %v", ErrInvalidRoleID, err) } - existingUser, _ := s.UserRepo.FindByUsername(user.Username) - if existingUser != nil { - return nil, fmt.Errorf("username is already taken") + if existingUser, _ := s.UserRepo.FindByUsername(user.Username); existingUser != nil { + return nil, fmt.Errorf("%s: %v", ErrUsernameTaken, nil) } - existingPhone, _ := s.UserRepo.FindByPhoneAndRole(user.Phone, user.RoleID) - if existingPhone != nil { - return nil, fmt.Errorf("phone number is already used for this role") + if existingPhone, _ := s.UserRepo.FindByPhoneAndRole(user.Phone, user.RoleID); existingPhone != nil { + return nil, fmt.Errorf("%s: %v", ErrPhoneTaken, nil) } - existingEmail, _ := s.UserRepo.FindByEmailAndRole(user.Email, user.RoleID) - if existingEmail != nil { - return nil, fmt.Errorf("email is already used for this role") + if existingEmail, _ := s.UserRepo.FindByEmailAndRole(user.Email, user.RoleID); existingEmail != nil { + return nil, fmt.Errorf("%s: %v", ErrEmailTaken, nil) } hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost) if err != nil { - return nil, fmt.Errorf("failed to hash password: %v", err) + return nil, fmt.Errorf("%s: %v", ErrFailedToHashPassword, err) } newUser := model.User{ @@ -135,10 +144,28 @@ func (s *userService) Register(user dto.RegisterDTO) (*model.User, error) { err = s.UserRepo.Create(&newUser) if err != nil { - return nil, fmt.Errorf("failed to create user: %v", err) + return nil, fmt.Errorf("%s: %v", ErrFailedToCreateUser, err) } - newUser.Role = *role + userResponse := s.prepareUserResponse(newUser, role) - return &newUser, nil -} \ No newline at end of file + return userResponse, nil +} + +func (s *userService) prepareUserResponse(user model.User, role *model.Role) *dto.UserResponseDTO { + + createdAt, _ := utils.FormatDateToIndonesianFormat(user.CreatedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(user.UpdatedAt) + + return &dto.UserResponseDTO{ + ID: user.ID, + Username: user.Username, + Name: user.Name, + Phone: user.Phone, + Email: user.Email, + EmailVerified: user.EmailVerified, + RoleName: role.RoleName, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + } +} diff --git a/model/user_model.go b/model/user_model.go index eb5143f..8f26826 100644 --- a/model/user_model.go +++ b/model/user_model.go @@ -12,7 +12,7 @@ type User struct { EmailVerified bool `gorm:"default:false" json:"emailVerified"` Password string `gorm:"not null" json:"password"` RoleID string `gorm:"not null" json:"roleId"` - Role Role `gorm:"foreignKey:RoleID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" json:"role"` + Role *Role `gorm:"foreignKey:RoleID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" json:"role"` CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"` UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"` } diff --git a/presentation/article_route.go b/presentation/article_route.go index e942dd3..f315f34 100644 --- a/presentation/article_route.go +++ b/presentation/article_route.go @@ -18,6 +18,7 @@ func ArticleRouter(api fiber.Router) { articleAPI := api.Group("/article-rijik") articleAPI.Post("/create-article", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator), articleHandler.CreateArticle) - articleAPI.Get("/view-article", middleware.AuthMiddleware, articleHandler.GetAllArticles) - articleAPI.Get("/view-article/:article_id", middleware.AuthMiddleware, articleHandler.GetArticleByID) + articleAPI.Get("/view-article", articleHandler.GetAllArticles) + articleAPI.Get("/view-article/:article_id", articleHandler.GetArticleByID) + articleAPI.Put("/update-article/:article_id", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator), articleHandler.UpdateArticle) } diff --git a/utils/redis_caching.go b/utils/redis_caching.go index 26999f4..e1f15d2 100644 --- a/utils/redis_caching.go +++ b/utils/redis_caching.go @@ -3,6 +3,7 @@ package utils import ( "context" "encoding/json" + "fmt" "log" "time" @@ -15,6 +16,7 @@ var ctx = context.Background() const defaultExpiration = 1 * time.Hour func SetData[T any](key string, value T, expiration time.Duration) error { + if expiration == 0 { expiration = defaultExpiration } @@ -26,7 +28,7 @@ func SetData[T any](key string, value T, expiration time.Duration) error { err = config.RedisClient.Set(ctx, key, jsonData, expiration).Err() if err != nil { - return logAndReturnError("Error setting data in Redis", err) + return logAndReturnError(fmt.Sprintf("Error setting data in Redis with key: %s", key), err) } log.Printf("Data stored in Redis with key: %s", key) @@ -40,7 +42,7 @@ func GetData(key string) (string, error) { return "", nil } else if err != nil { - return "", logAndReturnError("Error retrieving data from Redis", err) + return "", logAndReturnError(fmt.Sprintf("Error retrieving data from Redis with key: %s", key), err) } return val, nil } @@ -48,7 +50,7 @@ func GetData(key string) (string, error) { func DeleteData(key string) error { err := config.RedisClient.Del(ctx, key).Err() if err != nil { - return logAndReturnError("Error deleting data from Redis", err) + return logAndReturnError(fmt.Sprintf("Error deleting data from Redis with key: %s", key), err) } log.Printf("Data deleted from Redis with key: %s", key) return nil @@ -57,7 +59,7 @@ func DeleteData(key string) error { func CheckKeyExists(key string) (bool, error) { val, err := config.RedisClient.Exists(ctx, key).Result() if err != nil { - return false, logAndReturnError("Error checking if key exists in Redis", err) + return false, logAndReturnError(fmt.Sprintf("Error checking if key exists in Redis with key: %s", key), err) } return val > 0, nil }