diff --git a/dto/trash_dto.go b/dto/trash_dto.go index 085fe59..9df527f 100644 --- a/dto/trash_dto.go +++ b/dto/trash_dto.go @@ -4,13 +4,15 @@ import "strings" type RequestTrashCategoryDTO struct { Name string `json:"name"` + Icon string `json:"icon"` } type ResponseTrashCategoryDTO struct { - ID string `json:"id"` - Name string `json:"name"` - CreatedAt string `json:"createdAt"` - UpdatedAt string `json:"updatedAt"` + ID string `json:"id"` + Name string `json:"name"` + Icon string `json:"icon"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` Details []ResponseTrashDetailDTO `json:"details,omitempty"` } diff --git a/dto/user_dto.go b/dto/user_dto.go index 0d74533..9558bd6 100644 --- a/dto/user_dto.go +++ b/dto/user_dto.go @@ -1,7 +1,7 @@ package dto import ( - "regexp" + "rijig/utils" "strings" ) @@ -18,13 +18,13 @@ type UserResponseDTO struct { UpdatedAt string `json:"updatedAt"` } -type UpdateUserDTO struct { +type RequestUserDTO struct { Name string `json:"name"` Phone string `json:"phone"` Email string `json:"email"` } -func (r *UpdateUserDTO) Validate() (map[string][]string, bool) { +func (r *RequestUserDTO) Validate() (map[string][]string, bool) { errors := make(map[string][]string) if strings.TrimSpace(r.Name) == "" { @@ -33,10 +33,14 @@ func (r *UpdateUserDTO) Validate() (map[string][]string, bool) { if strings.TrimSpace(r.Phone) == "" { errors["phone"] = append(errors["phone"], "Phone number is required") - } else if !IsValidPhoneNumber(r.Phone) { + } else if !utils.IsValidPhoneNumber(r.Phone) { errors["phone"] = append(errors["phone"], "Invalid phone number format. Use +62 followed by 9-13 digits") } + if strings.TrimSpace(r.Email) != "" && !utils.IsValidEmail(r.Email) { + errors["email"] = append(errors["email"], "Invalid email format") + } + if len(errors) > 0 { return errors, false } @@ -44,18 +48,6 @@ func (r *UpdateUserDTO) Validate() (map[string][]string, bool) { return nil, true } -func IsUpdateValidPhoneNumber(phone string) bool { - - re := regexp.MustCompile(`^\+62\d{9,13}$`) - return re.MatchString(phone) -} - -func IsUPdateValidEmail(email string) bool { - - re := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) - return re.MatchString(email) -} - type UpdatePasswordDTO struct { OldPassword string `json:"old_password"` NewPassword string `json:"new_password"` @@ -71,8 +63,8 @@ func (u *UpdatePasswordDTO) Validate() (map[string][]string, bool) { if u.NewPassword == "" { errors["new_password"] = append(errors["new_password"], "New password is required") - } else if len(u.NewPassword) < 8 { - errors["new_password"] = append(errors["new_password"], "Password must be at least 8 characters long") + } else if !utils.IsValidPassword(u.NewPassword) { + errors["new_password"] = append(errors["new_password"], "Password must contain at least one uppercase letter, one digit, and one special character") } if u.ConfirmNewPassword == "" { diff --git a/internal/handler/trash_handler.go b/internal/handler/trash_handler.go index afc85b5..e3cace8 100644 --- a/internal/handler/trash_handler.go +++ b/internal/handler/trash_handler.go @@ -1,6 +1,7 @@ package handler import ( + "log" "rijig/dto" "rijig/internal/services" "rijig/utils" @@ -22,7 +23,13 @@ func (h *TrashHandler) CreateCategory(c *fiber.Ctx) error { return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}}) } - categoryResponse, err := h.TrashService.CreateCategory(request) + iconTrash, err := c.FormFile("icon") + if err != nil { + log.Printf("Error retrieving card photo from request: %v", err) + return utils.ErrorResponse(c, "Card photo is required") + } + + categoryResponse, err := h.TrashService.CreateCategory(request, iconTrash) if err != nil { return utils.GenericResponse(c, fiber.StatusInternalServerError, "Failed to create category: "+err.Error()) } @@ -84,7 +91,13 @@ func (h *TrashHandler) UpdateCategory(c *fiber.Ctx) error { return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid request body"}}) } - updatedCategory, err := h.TrashService.UpdateCategory(id, request) + iconTrash, err := c.FormFile("icon") + if err != nil && err.Error() != "File not found" { + log.Printf("Error retrieving icon trash from request: %v", err) + return utils.ErrorResponse(c, "icon trash is required") + } + + updatedCategory, err := h.TrashService.UpdateCategory(id, request, iconTrash) if err != nil { return utils.GenericResponse(c, fiber.StatusInternalServerError, "Error updating category: "+err.Error()) } diff --git a/internal/handler/user_handler.go b/internal/handler/user_handler.go index e479047..9b60bec 100644 --- a/internal/handler/user_handler.go +++ b/internal/handler/user_handler.go @@ -4,138 +4,98 @@ import ( "rijig/dto" "rijig/internal/services" "rijig/utils" + "strconv" "github.com/gofiber/fiber/v2" ) -type UserProfileHandler struct { - UserProfileService services.UserProfileService +type UserHandler struct { + userService services.UserService } -func NewUserProfileHandler(userProfileService services.UserProfileService) *UserProfileHandler { - return &UserProfileHandler{UserProfileService: userProfileService} +func NewUserHandler(userService services.UserService) *UserHandler { + return &UserHandler{userService: userService} } -func (h *UserProfileHandler) GetUserProfile(c *fiber.Ctx) error { +func (h *UserHandler) UpdateUserAvatarHandler(c *fiber.Ctx) error { + userID := c.Locals("userID").(string) + + avatar, err := c.FormFile("avatar") + if err != nil { + return utils.GenericResponse(c, fiber.StatusBadRequest, "No avatar file provided") + } + + updatedUser, err := h.userService.UpdateUserAvatar(userID, avatar) + if err != nil { + return utils.GenericResponse(c, fiber.StatusInternalServerError, err.Error()) + } + + return utils.SuccessResponse(c, updatedUser, "Avatar updated successfully") +} + +func (h *UserHandler) GetUserByIDHandler(c *fiber.Ctx) error { + // userID := c.Params("id") userID, ok := c.Locals("userID").(string) if !ok || userID == "" { return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") } - userProfile, err := h.UserProfileService.GetUserProfile(userID) + user, err := h.userService.GetUserByID(userID) if err != nil { return utils.GenericResponse(c, fiber.StatusNotFound, err.Error()) } - return utils.SuccessResponse(c, userProfile, "User profile retrieved successfully") + return utils.SuccessResponse(c, user, "User retrieved successfully") } -func (h *UserProfileHandler) GetUserProfileById(c *fiber.Ctx) error { - userID := c.Params("userid") - if userID == "" { - return utils.ValidationErrorResponse(c, map[string][]string{"userid": {"user ID is required"}}) +func (h *UserHandler) GetAllUsersHandler(c *fiber.Ctx) error { + + page := 1 + limit := 10 + + if p := c.Query("page"); p != "" { + page, _ = strconv.Atoi(p) } - // userID, ok := c.Locals("userID").(string) - // if !ok || userID == "" { - // return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") - // } - - userProfile, err := h.UserProfileService.GetUserProfile(userID) - if err != nil { - return utils.GenericResponse(c, fiber.StatusNotFound, err.Error()) + if l := c.Query("limit"); l != "" { + limit, _ = strconv.Atoi(l) } - return utils.SuccessResponse(c, userProfile, "User profile retrieved successfully") -} - -func (h *UserProfileHandler) GetAllUsers(c *fiber.Ctx) error { - users, err := h.UserProfileService.GetAllUsers() + users, err := h.userService.GetAllUsers(page, limit) if err != nil { return utils.GenericResponse(c, fiber.StatusInternalServerError, err.Error()) } - return utils.SuccessResponse(c, users, "All users retrieved successfully") + return utils.PaginatedResponse(c, users, page, limit, len(users), "Users retrieved successfully") } -func (h *UserProfileHandler) GetUsersByRoleID(c *fiber.Ctx) error { - roleID := c.Params("roleid") - if roleID == "" { - return utils.ValidationErrorResponse(c, map[string][]string{"roleId": {"Role ID is required"}}) +func (h *UserHandler) UpdateUserHandler(c *fiber.Ctx) error { + var request dto.RequestUserDTO + if err := c.BodyParser(&request); err != nil { + return utils.GenericResponse(c, fiber.StatusBadRequest, "Invalid request body") } - users, err := h.UserProfileService.GetUsersByRoleID(roleID) + userID := c.Locals("userID").(string) + updatedUser, err := h.userService.UpdateUser(userID, &request) if err != nil { return utils.GenericResponse(c, fiber.StatusInternalServerError, err.Error()) } - return utils.SuccessResponse(c, users, "Users retrieved successfully") + return utils.SuccessResponse(c, updatedUser, "User profile updated successfully") } -func (h *UserProfileHandler) UpdateUserProfile(c *fiber.Ctx) error { - var updateData dto.UpdateUserDTO - if err := c.BodyParser(&updateData); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}}) +func (h *UserHandler) UpdateUserPasswordHandler(c *fiber.Ctx) error { + var request dto.UpdatePasswordDTO + if err := c.BodyParser(&request); err != nil { + return utils.GenericResponse(c, fiber.StatusBadRequest, "Invalid request body") } - userID, ok := c.Locals("userID").(string) - if !ok || userID == "" { - return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") - } - - errors, valid := updateData.Validate() - if !valid { - return utils.ValidationErrorResponse(c, errors) - } - - userResponse, err := h.UserProfileService.UpdateUserProfile(userID, updateData) - if err != nil { - return utils.GenericResponse(c, fiber.StatusConflict, err.Error()) - } - - return utils.SuccessResponse(c, userResponse, "User profile updated successfully") -} - -// func (h *UserProfileHandler) UpdateUserPassword(c *fiber.Ctx) error { -// var passwordData dto.UpdatePasswordDTO -// if err := c.BodyParser(&passwordData); err != nil { -// return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}}) -// } - -// userID, ok := c.Locals("userID").(string) -// if !ok || userID == "" { -// return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") -// } - -// errors, valid := passwordData.Validate() -// if !valid { -// return utils.ValidationErrorResponse(c, errors) -// } - -// message, err := h.UserProfileService.UpdateUserPassword(userID, passwordData) -// if err != nil { -// return utils.GenericResponse(c, fiber.StatusBadRequest, err.Error()) -// } - -// return utils.GenericResponse(c, fiber.StatusOK, message) -// } -func (h *UserProfileHandler) UpdateUserAvatar(c *fiber.Ctx) error { - - userID, ok := c.Locals("userID").(string) - if !ok || userID == "" { - return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") - } - - file, err := c.FormFile("avatar") - if err != nil { - return utils.GenericResponse(c, fiber.StatusBadRequest, "No avatar file uploaded") - } - - message, err := h.UserProfileService.UpdateUserAvatar(userID, file) + userID := c.Locals("userID").(string) + err := h.userService.UpdateUserPassword(userID, request.OldPassword, request.NewPassword, request.ConfirmNewPassword) if err != nil { return utils.GenericResponse(c, fiber.StatusInternalServerError, err.Error()) } - return utils.GenericResponse(c, fiber.StatusOK, message) + return utils.SuccessResponse(c, nil, "Password updated successfully") } diff --git a/internal/repositories/trash_repo.go b/internal/repositories/trash_repo.go index 1fd4861..380716a 100644 --- a/internal/repositories/trash_repo.go +++ b/internal/repositories/trash_repo.go @@ -1,7 +1,9 @@ package repositories import ( + "errors" "fmt" + "log" "rijig/model" @@ -16,6 +18,7 @@ type TrashRepository interface { GetTrashDetailByID(id string) (*model.TrashDetail, error) GetDetailsByCategoryID(categoryID string) ([]model.TrashDetail, error) UpdateCategoryName(id string, newName string) error + UpdateCategory(id string, updateTrashCategory *model.TrashCategory) (*model.TrashCategory, error) UpdateTrashDetail(id string, description string, price float64) error DeleteCategory(id string) error DeleteTrashDetail(id string) error @@ -84,6 +87,23 @@ func (r *trashRepository) UpdateCategoryName(id string, newName string) error { return nil } +func (r *trashRepository) UpdateCategory(id string, updateTrashCategory *model.TrashCategory) (*model.TrashCategory, error) { + var existingtrashCtgry model.TrashCategory + if err := r.DB.Where("id = ?", id).First(&existingtrashCtgry).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("trashCategory with ID %s not found", id) + } + log.Printf("Error fetching trash category for update: %v", err) + return nil, fmt.Errorf("error fetching trash category for update: %w", err) + } + + if err := r.DB.Save(&existingtrashCtgry).Error; err != nil { + log.Printf("Error updating trash category: %v", err) + return nil, fmt.Errorf("failed to update trash category: %w", err) + } + return &existingtrashCtgry, nil +} + func (r *trashRepository) UpdateTrashDetail(id string, description string, price float64) error { if err := r.DB.Model(&model.TrashDetail{}).Where("id = ?", id).Updates(model.TrashDetail{Description: description, Price: price}).Error; err != nil { return fmt.Errorf("failed to update trash detail: %v", err) diff --git a/internal/repositories/user_repo.go b/internal/repositories/user_repo.go index 4243219..8c7e085 100644 --- a/internal/repositories/user_repo.go +++ b/internal/repositories/user_repo.go @@ -2,77 +2,76 @@ package repositories import ( "fmt" - "rijig/model" "gorm.io/gorm" ) -type UserProfileRepository interface { +type UserProfilRepository interface { FindByID(userID string) (*model.User, error) + FindAll(page, limit int) ([]model.User, error) Update(user *model.User) error UpdateAvatar(userID, avatarURL string) error - - FindAll() ([]model.User, error) - FindByRoleID(roleID string) ([]model.User, error) + UpdatePassword(userID string, newPassword string) error } -type userProfileRepository struct { +type userProfilRepository struct { DB *gorm.DB } -func NewUserProfileRepository(db *gorm.DB) UserProfileRepository { - return &userProfileRepository{DB: db} +func NewUserProfilRepository(db *gorm.DB) UserProfilRepository { + return &userProfilRepository{DB: db} } -func (r *userProfileRepository) FindByID(userID string) (*model.User, error) { +func (r *userProfilRepository) FindByID(userID string) (*model.User, error) { var user model.User err := r.DB.Preload("Role").Where("id = ?", userID).First(&user).Error if err != nil { if err == gorm.ErrRecordNotFound { return nil, fmt.Errorf("user with ID %s not found", userID) } - return nil, err + return nil, fmt.Errorf("error finding user with ID %s: %v", userID, err) } if user.Role == nil { - return nil, fmt.Errorf("role not found for this user") + return nil, fmt.Errorf("role not found for user ID %s", userID) } return &user, nil } -func (r *userProfileRepository) Update(user *model.User) error { +func (r *userProfilRepository) FindAll(page, limit int) ([]model.User, error) { + var users []model.User + offset := (page - 1) * limit + err := r.DB.Preload("Role").Offset(offset).Limit(limit).Find(&users).Error + if err != nil { + return nil, fmt.Errorf("error finding all users: %v", err) + } + return users, nil +} + +func (r *userProfilRepository) Update(user *model.User) error { err := r.DB.Save(user).Error if err != nil { - return err + return fmt.Errorf("error updating user: %v", err) } return nil } -func (r *userProfileRepository) UpdateAvatar(userID, avatarURL string) error { +func (r *userProfilRepository) UpdateAvatar(userID, avatarURL string) error { var user model.User err := r.DB.Model(&user).Where("id = ?", userID).Update("avatar", avatarURL).Error if err != nil { - return err + return fmt.Errorf("error updating avatar for user ID %s: %v", userID, err) } return nil } -func (r *userProfileRepository) FindAll() ([]model.User, error) { - var users []model.User - err := r.DB.Preload("Role").Find(&users).Error +func (r *userProfilRepository) UpdatePassword(userID string, newPassword string) error { + var user model.User + err := r.DB.Model(&user).Where("id = ?", userID).Update("password", newPassword).Error if err != nil { - return nil, err + return fmt.Errorf("error updating password for user ID %s: %v", userID, err) } - return users, nil -} - -func (r *userProfileRepository) FindByRoleID(roleID string) ([]model.User, error) { - var users []model.User - err := r.DB.Preload("Role").Where("role_id = ?", roleID).Find(&users).Error - if err != nil { - return nil, err - } - return users, nil + return nil } diff --git a/internal/services/identitycard_service.go b/internal/services/identitycard_service.go index f6b62ff..b2435cd 100644 --- a/internal/services/identitycard_service.go +++ b/internal/services/identitycard_service.go @@ -22,10 +22,10 @@ type IdentityCardService interface { type identityCardService struct { identityCardRepo repositories.IdentityCardRepository - userRepo repositories.UserProfileRepository + userRepo repositories.UserProfilRepository } -func NewIdentityCardService(identityCardRepo repositories.IdentityCardRepository, userRepo repositories.UserProfileRepository) IdentityCardService { +func NewIdentityCardService(identityCardRepo repositories.IdentityCardRepository, userRepo repositories.UserProfilRepository) IdentityCardService { return &identityCardService{ identityCardRepo: identityCardRepo, userRepo: userRepo, diff --git a/internal/services/trash_service.go b/internal/services/trash_service.go index dcc05ca..cd66add 100644 --- a/internal/services/trash_service.go +++ b/internal/services/trash_service.go @@ -2,23 +2,29 @@ package services import ( "fmt" + "log" + "mime/multipart" + "os" + "path/filepath" "time" "rijig/dto" "rijig/internal/repositories" "rijig/model" "rijig/utils" + + "github.com/google/uuid" ) type TrashService interface { - CreateCategory(request dto.RequestTrashCategoryDTO) (*dto.ResponseTrashCategoryDTO, error) + CreateCategory(request dto.RequestTrashCategoryDTO, iconTrash *multipart.FileHeader) (*dto.ResponseTrashCategoryDTO, error) AddDetailToCategory(request dto.RequestTrashDetailDTO) (*dto.ResponseTrashDetailDTO, error) GetCategories() ([]dto.ResponseTrashCategoryDTO, error) GetCategoryByID(id string) (*dto.ResponseTrashCategoryDTO, error) GetTrashDetailByID(id string) (*dto.ResponseTrashDetailDTO, error) - UpdateCategory(id string, request dto.RequestTrashCategoryDTO) (*dto.ResponseTrashCategoryDTO, error) + UpdateCategory(id string, request dto.RequestTrashCategoryDTO, iconPath *multipart.FileHeader) (*dto.ResponseTrashCategoryDTO, error) UpdateDetail(id string, request dto.RequestTrashDetailDTO) (*dto.ResponseTrashDetailDTO, error) DeleteCategory(id string) error @@ -33,14 +39,81 @@ func NewTrashService(trashRepo repositories.TrashRepository) TrashService { return &trashService{TrashRepo: trashRepo} } -func (s *trashService) CreateCategory(request dto.RequestTrashCategoryDTO) (*dto.ResponseTrashCategoryDTO, error) { +func (s *trashService) saveIconOfTrash(iconTrash *multipart.FileHeader) (string, error) { + pathImage := "/uploads/icontrash/" + iconTrashDir := "./public" + os.Getenv("BASE_URL") + pathImage + if _, err := os.Stat(iconTrashDir); os.IsNotExist(err) { + + if err := os.MkdirAll(iconTrashDir, os.ModePerm); err != nil { + return "", fmt.Errorf("failed to create directory for icon trash: %v", err) + } + } + + allowedExtensions := map[string]bool{".jpg": true, ".jpeg": true, ".png": true, ".svg": true} + extension := filepath.Ext(iconTrash.Filename) + if !allowedExtensions[extension] { + return "", fmt.Errorf("invalid file type, only .jpg, .jpeg, and .png are allowed") + } + + iconTrashFIleName := fmt.Sprintf("%s_icontrash%s", uuid.New().String(), extension) + iconTrashPath := filepath.Join(iconTrashDir, iconTrashFIleName) + + src, err := iconTrash.Open() + if err != nil { + return "", fmt.Errorf("failed to open uploaded file: %v", err) + } + defer src.Close() + + dst, err := os.Create(iconTrashPath) + if err != nil { + return "", fmt.Errorf("failed to create icon trash file: %v", err) + } + defer dst.Close() + + if _, err := dst.ReadFrom(src); err != nil { + return "", fmt.Errorf("failed to save icon trash: %v", err) + } + + iconTrashUrl := fmt.Sprintf("%s%s", pathImage, iconTrashFIleName) + + return iconTrashUrl, nil +} + +func deleteIconTrashFIle(imagePath string) error { + if imagePath == "" { + return nil + } + + baseDir := "./public/" + os.Getenv("BASE_URL") + absolutePath := baseDir + imagePath + + if _, err := os.Stat(absolutePath); os.IsNotExist(err) { + return fmt.Errorf("image file not found: %v", err) + } + + err := os.Remove(absolutePath) + if err != nil { + return fmt.Errorf("failed to delete image: %v", err) + } + + log.Printf("Image deleted successfully: %s", absolutePath) + return nil +} + +func (s *trashService) CreateCategory(request dto.RequestTrashCategoryDTO, iconTrash *multipart.FileHeader) (*dto.ResponseTrashCategoryDTO, error) { errors, valid := request.ValidateTrashCategoryInput() if !valid { return nil, fmt.Errorf("validation error: %v", errors) } + icontrashPath, err := s.saveIconOfTrash(iconTrash) + if err != nil { + return nil, fmt.Errorf("gagal menyimpan ikon sampah: %v", err) + } + category := model.TrashCategory{ Name: request.Name, + Icon: icontrashPath, } if err := s.TrashRepo.CreateCategory(&category); err != nil { @@ -53,6 +126,7 @@ func (s *trashService) CreateCategory(request dto.RequestTrashCategoryDTO) (*dto categoryResponseDTO := &dto.ResponseTrashCategoryDTO{ ID: category.ID, Name: category.Name, + Icon: category.Icon, CreatedAt: createdAt, UpdatedAt: updatedAt, } @@ -70,6 +144,7 @@ func (s *trashService) CreateCategory(request dto.RequestTrashCategoryDTO) (*dto categoriesDTO = append(categoriesDTO, dto.ResponseTrashCategoryDTO{ ID: c.ID, Name: c.Name, + Icon: c.Icon, CreatedAt: ccreatedAt, UpdatedAt: cupdatedAt, }) @@ -129,6 +204,7 @@ func (s *trashService) AddDetailToCategory(request dto.RequestTrashDetailDTO) (* categoryResponseDTO := &dto.ResponseTrashCategoryDTO{ ID: category.ID, Name: category.Name, + Icon: category.Icon, CreatedAt: ccreatedAt, UpdatedAt: cupdatedAt, } @@ -153,6 +229,7 @@ func (s *trashService) GetCategories() ([]dto.ResponseTrashCategoryDTO, error) { categoriesDTO = append(categoriesDTO, dto.ResponseTrashCategoryDTO{ ID: categoryData["id"].(string), Name: categoryData["name"].(string), + Icon: categoryData["icon"].(string), CreatedAt: categoryData["createdAt"].(string), UpdatedAt: categoryData["updatedAt"].(string), }) @@ -172,6 +249,7 @@ func (s *trashService) GetCategories() ([]dto.ResponseTrashCategoryDTO, error) { categoriesDTO = append(categoriesDTO, dto.ResponseTrashCategoryDTO{ ID: category.ID, Name: category.Name, + Icon: category.Icon, CreatedAt: createdAt, UpdatedAt: updatedAt, }) @@ -196,6 +274,7 @@ func (s *trashService) GetCategoryByID(id string) (*dto.ResponseTrashCategoryDTO return &dto.ResponseTrashCategoryDTO{ ID: categoryData["id"].(string), Name: categoryData["name"].(string), + Icon: categoryData["icon"].(string), CreatedAt: categoryData["createdAt"].(string), UpdatedAt: categoryData["updatedAt"].(string), Details: details, @@ -213,6 +292,7 @@ func (s *trashService) GetCategoryByID(id string) (*dto.ResponseTrashCategoryDTO categoryDTO := &dto.ResponseTrashCategoryDTO{ ID: category.ID, Name: category.Name, + Icon: category.Icon, CreatedAt: createdAt, UpdatedAt: updatedAt, } @@ -220,13 +300,15 @@ func (s *trashService) GetCategoryByID(id string) (*dto.ResponseTrashCategoryDTO if category.Details != nil { var detailsDTO []dto.ResponseTrashDetailDTO for _, detail := range category.Details { + createdAt, _ := utils.FormatDateToIndonesianFormat(detail.CreatedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(detail.UpdatedAt) detailsDTO = append(detailsDTO, dto.ResponseTrashDetailDTO{ ID: detail.ID, CategoryID: detail.CategoryID, Description: detail.Description, Price: detail.Price, - CreatedAt: detail.CreatedAt.Format("02-01-2006 15:04"), - UpdatedAt: detail.UpdatedAt.Format("02-01-2006 15:04"), + CreatedAt: createdAt, + UpdatedAt: updatedAt, }) } categoryDTO.Details = detailsDTO @@ -281,27 +363,49 @@ func (s *trashService) GetTrashDetailByID(id string) (*dto.ResponseTrashDetailDT return detailDTO, nil } -func (s *trashService) UpdateCategory(id string, request dto.RequestTrashCategoryDTO) (*dto.ResponseTrashCategoryDTO, error) { +func (s *trashService) UpdateCategory(id string, request dto.RequestTrashCategoryDTO, iconPath *multipart.FileHeader) (*dto.ResponseTrashCategoryDTO, error) { errors, valid := request.ValidateTrashCategoryInput() if !valid { return nil, fmt.Errorf("validation error: %v", errors) } - if err := s.TrashRepo.UpdateCategoryName(id, request.Name); err != nil { - return nil, fmt.Errorf("failed to update category: %v", err) - } - category, err := s.TrashRepo.GetCategoryByID(id) if err != nil { return nil, fmt.Errorf("category not found: %v", err) } + if category.Icon != "" { + err := deleteIconTrashFIle(category.Icon) + if err != nil { + return nil, fmt.Errorf("failed to delete old image: %v", err) + } + } + + var iconTrashPath string + if iconPath != nil { + iconTrashPath, err = s.saveIconOfTrash(iconPath) + if err != nil { + return nil, fmt.Errorf("failed to save card photo: %v", err) + } + } + + if iconTrashPath != "" { + category.Icon = iconTrashPath + } + + category, err = s.TrashRepo.UpdateCategory(id, category) + if err != nil { + log.Printf("Error updating trash category: %v", err) + return nil, fmt.Errorf("failed to update category: %v", err) + } + createdAt, _ := utils.FormatDateToIndonesianFormat(category.CreatedAt) updatedAt, _ := utils.FormatDateToIndonesianFormat(category.UpdatedAt) categoryResponseDTO := &dto.ResponseTrashCategoryDTO{ ID: category.ID, Name: category.Name, + Icon: category.Icon, CreatedAt: createdAt, UpdatedAt: updatedAt, } @@ -319,6 +423,7 @@ func (s *trashService) UpdateCategory(id string, request dto.RequestTrashCategor categoriesDTO = append(categoriesDTO, dto.ResponseTrashCategoryDTO{ ID: c.ID, Name: c.Name, + Icon: c.Icon, CreatedAt: ccreatedAt, UpdatedAt: cupdatedAt, }) @@ -376,6 +481,7 @@ func (s *trashService) UpdateDetail(id string, request dto.RequestTrashDetailDTO categoryResponseDTO := &dto.ResponseTrashCategoryDTO{ ID: category.ID, Name: category.Name, + Icon: category.Icon, CreatedAt: ccreatedAt, UpdatedAt: cupdatedAt, } diff --git a/internal/services/user_service.go b/internal/services/user_service.go index 72a5994..05c2774 100644 --- a/internal/services/user_service.go +++ b/internal/services/user_service.go @@ -1,310 +1,193 @@ package services import ( - "encoding/json" - "errors" "fmt" "log" "mime/multipart" "os" "path/filepath" - "time" - "rijig/dto" "rijig/internal/repositories" "rijig/model" "rijig/utils" - // "golang.org/x/crypto/bcrypt" ) -var allowedExtensions = []string{".jpg", ".jpeg", ".png"} - -type UserProfileService interface { - GetUserProfile(userID string) (*dto.UserResponseDTO, error) - UpdateUserProfile(userID string, updateData dto.UpdateUserDTO) (*dto.UserResponseDTO, error) - // UpdateUserPassword(userID string, passwordData dto.UpdatePasswordDTO) (string, error) - UpdateUserAvatar(userID string, file *multipart.FileHeader) (string, error) - - GetAllUsers() ([]dto.UserResponseDTO, error) - GetUsersByRoleID(roleID string) ([]dto.UserResponseDTO, error) +type UserService interface { + GetUserByID(userID string) (*dto.UserResponseDTO, error) + GetAllUsers(page, limit int) ([]dto.UserResponseDTO, error) + UpdateUser(userID string, request *dto.RequestUserDTO) (*dto.UserResponseDTO, error) + UpdateUserAvatar(userID string, avatar *multipart.FileHeader) (*dto.UserResponseDTO, error) + UpdateUserPassword(userID, oldPassword, newPassword, confirmNewPassword string) error } -type userProfileService struct { - UserRepo repositories.UserRepository - RoleRepo repositories.RoleRepository - UserProfileRepo repositories.UserProfileRepository +type userService struct { + userRepo repositories.UserProfilRepository } -func NewUserProfileService(userProfileRepo repositories.UserProfileRepository) UserProfileService { - return &userProfileService{UserProfileRepo: userProfileRepo} +func NewUserService(userRepo repositories.UserProfilRepository) UserService { + return &userService{userRepo: userRepo} } -func (s *userProfileService) prepareUserResponse(user *model.User) *dto.UserResponseDTO { +func (s *userService) GetUserByID(userID string) (*dto.UserResponseDTO, error) { + user, err := s.userRepo.FindByID(userID) + if err != nil { + return nil, fmt.Errorf("error retrieving user by ID: %v", err) + } + + userDTO, err := s.formatUserResponse(user) + if err != nil { + return nil, fmt.Errorf("error formatting user response: %v", err) + } + + return userDTO, nil +} + +func (s *userService) GetAllUsers(page, limit int) ([]dto.UserResponseDTO, error) { + users, err := s.userRepo.FindAll(page, limit) + if err != nil { + return nil, fmt.Errorf("error retrieving all users: %v", err) + } + + var userDTOs []dto.UserResponseDTO + for _, user := range users { + userDTO, err := s.formatUserResponse(&user) + if err != nil { + log.Printf("Error formatting user response for userID %s: %v", user.ID, err) + continue + } + userDTOs = append(userDTOs, *userDTO) + } + + return userDTOs, nil +} + +func (s *userService) UpdateUser(userID string, request *dto.RequestUserDTO) (*dto.UserResponseDTO, error) { + + errors, valid := request.Validate() + if !valid { + return nil, fmt.Errorf("validation failed: %v", errors) + } + + user, err := s.userRepo.FindByID(userID) + if err != nil { + return nil, fmt.Errorf("user not found: %v", err) + } + + user.Name = request.Name + user.Phone = request.Phone + user.Email = request.Email + + err = s.userRepo.Update(user) + if err != nil { + return nil, fmt.Errorf("error updating user: %v", err) + } + + userDTO, err := s.formatUserResponse(user) + if err != nil { + return nil, fmt.Errorf("error formatting updated user response: %v", err) + } + + return userDTO, nil +} + +func (s *userService) UpdateUserAvatar(userID string, avatar *multipart.FileHeader) (*dto.UserResponseDTO, error) { + + user, err := s.userRepo.FindByID(userID) + if err != nil { + return nil, fmt.Errorf("user not found: %v", err) + } + + if *user.Avatar != "" { + err := s.deleteAvatarImage(*user.Avatar) + if err != nil { + return nil, fmt.Errorf("failed to delete old image: %v", err) + } + } + + avatarURL, err := s.saveAvatarImage(userID, avatar) + if err != nil { + return nil, fmt.Errorf("failed to save avatar image: %v", err) + } + + err = s.userRepo.UpdateAvatar(userID, avatarURL) + if err != nil { + return nil, fmt.Errorf("failed to update avatar in the database: %v", err) + } + + userDTO, err := s.formatUserResponse(user) + if err != nil { + return nil, fmt.Errorf("failed to format user response: %v", err) + } + + return userDTO, nil +} + +func (s *userService) UpdateUserPassword(userID, oldPassword, newPassword, confirmNewPassword string) error { + + // errors, valid := utils.ValidatePasswordUpdate(oldPassword, newPassword, confirmNewPassword) + // if !valid { + // return fmt.Errorf("password validation error: %v", errors) + // } + + user, err := s.userRepo.FindByID(userID) + if err != nil { + return fmt.Errorf("user not found: %v", err) + } + + if user.Password != oldPassword { + return fmt.Errorf("old password is incorrect") + } + + err = s.userRepo.UpdatePassword(userID, newPassword) + if err != nil { + return fmt.Errorf("error updating password: %v", err) + } + + return nil +} + +func (s *userService) formatUserResponse(user *model.User) (*dto.UserResponseDTO, error) { + createdAt, _ := utils.FormatDateToIndonesianFormat(user.CreatedAt) updatedAt, _ := utils.FormatDateToIndonesianFormat(user.UpdatedAt) - return &dto.UserResponseDTO{ + userDTO := &dto.UserResponseDTO{ ID: user.ID, - // Username: user.Username, + Username: user.Name, Avatar: user.Avatar, Name: user.Name, Phone: user.Phone, Email: user.Email, - // EmailVerified: user.EmailVerified, + EmailVerified: user.PhoneVerified, RoleName: user.Role.RoleName, CreatedAt: createdAt, UpdatedAt: updatedAt, } + + return userDTO, nil } -func (s *userProfileService) GetUserProfile(userID string) (*dto.UserResponseDTO, error) { +func (s *userService) saveAvatarImage(userID string, avatar *multipart.FileHeader) (string, error) { - cacheKey := fmt.Sprintf("userProfile:%s", userID) - cachedData, err := utils.GetJSONData(cacheKey) - if err == nil && cachedData != nil { + pathImage := "/uploads/avatars/" + avatarDir := "./public" + os.Getenv("BASE_URL") + pathImage - 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") - } - - userResponse := s.prepareUserResponse(user) - - 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 -} - -func (s *userProfileService) GetAllUsers() ([]dto.UserResponseDTO, error) { - users, err := s.UserProfileRepo.FindAll() - if err != nil { - return nil, err - } - - var response []dto.UserResponseDTO - for _, user := range users { - response = append(response, dto.UserResponseDTO{ - ID: user.ID, - // Username: user.Username, - Avatar: user.Avatar, - Name: user.Name, - Phone: user.Phone, - // Email: user.Email, - // EmailVerified: user.EmailVerified, - RoleName: user.Role.RoleName, - CreatedAt: user.CreatedAt.Format(time.RFC3339), - UpdatedAt: user.UpdatedAt.Format(time.RFC3339), - }) - } - - return response, nil -} - -func (s *userProfileService) GetUsersByRoleID(roleID string) ([]dto.UserResponseDTO, error) { - users, err := s.UserProfileRepo.FindByRoleID(roleID) - if err != nil { - return nil, err - } - - var response []dto.UserResponseDTO - for _, user := range users { - response = append(response, dto.UserResponseDTO{ - ID: user.ID, - // Username: user.Username, - Avatar: user.Avatar, - Name: user.Name, - Phone: user.Phone, - // Email: user.Email, - // EmailVerified: user.EmailVerified, - RoleName: user.Role.RoleName, - CreatedAt: user.CreatedAt.Format(time.RFC3339), - UpdatedAt: user.UpdatedAt.Format(time.RFC3339), - }) - } - - return response, nil -} - -func (s *userProfileService) UpdateUserProfile(userID string, updateData dto.UpdateUserDTO) (*dto.UserResponseDTO, error) { - user, err := s.UserProfileRepo.FindByID(userID) - if err != nil { - return nil, errors.New("user not found") - } - - validationErrors, valid := updateData.Validate() - if !valid { - return nil, fmt.Errorf("validation failed: %v", validationErrors) - } - - if updateData.Name != "" { - user.Name = updateData.Name - } - - // if updateData.Phone != "" && updateData.Phone != user.Phone { - // if err := s.updatePhoneIfNeeded(user, updateData.Phone); err != nil { - // return nil, err - // } - // user.Phone = updateData.Phone - // } - - // if updateData.Email != "" && updateData.Email != user.Email { - // if err := s.updateEmailIfNeeded(user, updateData.Email); err != nil { - // return nil, err - // } - // user.Email = updateData.Email - // } - - err = s.UserProfileRepo.Update(user) - if err != nil { - return nil, fmt.Errorf("failed to update user: %v", err) - } - - userResponse := s.prepareUserResponse(user) - - cacheKey := fmt.Sprintf("userProfile:%s", userID) - cacheData := map[string]interface{}{ - "data": userResponse, - } - err = utils.SetJSONData(cacheKey, cacheData, time.Hour*24) - if err != nil { - fmt.Printf("Error updating cached user profile in Redis: %v\n", err) - } - - return userResponse, nil -} - -// func (s *userProfileService) updatePhoneIfNeeded(user *model.User, newPhone string) error { -// existingPhone, _ := s.UserRepo.FindByPhoneAndRole(newPhone, user.RoleID) -// if existingPhone != nil { -// return fmt.Errorf("phone number is already used for this role") -// } -// return nil -// } - -// func (s *userProfileService) updateEmailIfNeeded(user *model.User, newEmail string) error { -// existingEmail, _ := s.UserRepo.FindByEmailAndRole(newEmail, user.RoleID) -// if existingEmail != nil { -// return fmt.Errorf("email is already used for this role") -// } -// return nil -// } - -// func (s *userProfileService) UpdateUserPassword(userID string, passwordData dto.UpdatePasswordDTO) (string, error) { - -// validationErrors, valid := passwordData.Validate() -// if !valid { -// return "", fmt.Errorf("validation failed: %v", validationErrors) -// } - -// user, err := s.UserProfileRepo.FindByID(userID) -// if err != nil { -// return "", errors.New("user not found") -// } - -// if !CheckPasswordHash(passwordData.OldPassword, user.Password) { -// return "", errors.New("old password is incorrect") -// } - -// hashedPassword, err := bcrypt.GenerateFromPassword([]byte(passwordData.NewPassword), bcrypt.DefaultCost) -// if err != nil { -// return "", fmt.Errorf("failed to hash new password: %v", err) -// } - -// user.Password = string(hashedPassword) -// err = s.UserProfileRepo.Update(user) -// if err != nil { -// return "", fmt.Errorf("failed to update password: %v", err) -// } - -// return "Password berhasil diupdate", nil -// } - -func (s *userProfileService) UpdateUserAvatar(userID string, file *multipart.FileHeader) (string, error) { - baseURL := os.Getenv("BASE_URL") - if baseURL == "" { - return "", fmt.Errorf("BASE_URL is not set in environment variables") - } - - avatarDir := filepath.Join("./public", baseURL, "/uploads/avatars") - if err := ensureAvatarDirectoryExists(avatarDir); err != nil { - return "", err - } - - if err := validateAvatarFile(file); err != nil { - return "", err - } - - updatedUser, err := s.UserProfileRepo.FindByID(userID) - if err != nil { - return "", fmt.Errorf("failed to retrieve user data: %v", err) - } - - if updatedUser.Avatar != nil && *updatedUser.Avatar != "" { - oldAvatarPath := filepath.Join("./public", *updatedUser.Avatar) - if _, err := os.Stat(oldAvatarPath); err == nil { - - if err := os.Remove(oldAvatarPath); err != nil { - return "", fmt.Errorf("failed to remove old avatar: %v", err) - } - } else { - - log.Printf("Old avatar file not found: %s", oldAvatarPath) - } - } - - avatarURL, err := saveAvatarFile(file, userID, avatarDir) - if err != nil { - return "", err - } - - err = s.UserProfileRepo.UpdateAvatar(userID, avatarURL) - if err != nil { - return "", fmt.Errorf("failed to update avatar in the database: %v", err) - } - - return "Foto profil berhasil diupdate", nil -} - -func ensureAvatarDirectoryExists(avatarDir string) error { if _, err := os.Stat(avatarDir); os.IsNotExist(err) { if err := os.MkdirAll(avatarDir, os.ModePerm); err != nil { - return fmt.Errorf("failed to create avatar directory: %v", err) + return "", fmt.Errorf("failed to create directory for avatar: %v", err) } } - return nil -} -func validateAvatarFile(file *multipart.FileHeader) error { - extension := filepath.Ext(file.Filename) - for _, ext := range allowedExtensions { - if extension == ext { - return nil - } + allowedExtensions := map[string]bool{".jpg": true, ".jpeg": true, ".png": true} + extension := filepath.Ext(avatar.Filename) + if !allowedExtensions[extension] { + return "", fmt.Errorf("invalid file type, only .jpg, .jpeg, and .png are allowed") } - return fmt.Errorf("invalid file type, only .jpg, .jpeg, and .png are allowed") -} -func saveAvatarFile(file *multipart.FileHeader, userID, avatarDir string) (string, error) { - extension := filepath.Ext(file.Filename) avatarFileName := fmt.Sprintf("%s_avatar%s", userID, extension) avatarPath := filepath.Join(avatarDir, avatarFileName) - src, err := file.Open() + src, err := avatar.Open() if err != nil { return "", fmt.Errorf("failed to open uploaded file: %v", err) } @@ -312,15 +195,37 @@ func saveAvatarFile(file *multipart.FileHeader, userID, avatarDir string) (strin dst, err := os.Create(avatarPath) if err != nil { - return "", fmt.Errorf("failed to create file: %v", err) + return "", fmt.Errorf("failed to create avatar file: %v", err) } defer dst.Close() - _, err = dst.ReadFrom(src) - if err != nil { - return "", fmt.Errorf("failed to save avatar file: %v", err) + if _, err := dst.ReadFrom(src); err != nil { + return "", fmt.Errorf("failed to save avatar: %v", err) } - relativePath := filepath.Join("/uploads/avatars", avatarFileName) - return relativePath, nil + avatarURL := fmt.Sprintf("%s%s", pathImage, avatarFileName) + + return avatarURL, nil +} + +func (s *userService) deleteAvatarImage(avatarPath string) error { + + if avatarPath == "" { + return nil + } + + baseDir := "./public/" + os.Getenv("BASE_URL") + absolutePath := baseDir + avatarPath + + if _, err := os.Stat(absolutePath); os.IsNotExist(err) { + return fmt.Errorf("image file not found: %v", err) + } + + err := os.Remove(absolutePath) + if err != nil { + return fmt.Errorf("failed to delete avatar image: %v", err) + } + + log.Printf("Avatar image deleted successfully: %s", absolutePath) + return nil } diff --git a/model/role_model.go b/model/role_model.go index 72c910e..14c6ca0 100644 --- a/model/role_model.go +++ b/model/role_model.go @@ -3,9 +3,8 @@ package model import "time" type Role struct { - ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` - RoleName string `gorm:"unique;not null" json:"roleName"` - // Users []User `gorm:"foreignKey:RoleID" json:"users"` + ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` + RoleName string `gorm:"unique;not null" json:"roleName"` CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"` UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"` } diff --git a/model/trash_model.go b/model/trash_model.go index 5bd05fb..7a90d0d 100644 --- a/model/trash_model.go +++ b/model/trash_model.go @@ -5,6 +5,7 @@ import "time" type TrashCategory struct { ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4()" json:"id"` Name string `gorm:"not null" json:"name"` + Icon string `json:"icon,omitempty"` Details []TrashDetail `gorm:"foreignKey:CategoryID;constraint:OnDelete:CASCADE;" json:"details"` CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"` UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"` diff --git a/presentation/identitycard_route.go b/presentation/identitycard_route.go index d77b848..16a00a2 100644 --- a/presentation/identitycard_route.go +++ b/presentation/identitycard_route.go @@ -13,7 +13,7 @@ import ( func IdentityCardRouter(api fiber.Router) { identityCardRepo := repositories.NewIdentityCardRepository(config.DB) - userRepo := repositories.NewUserProfileRepository(config.DB) + userRepo := repositories.NewUserProfilRepository(config.DB) identityCardService := services.NewIdentityCardService(identityCardRepo, userRepo) identityCardHandler := handler.NewIdentityCardHandler(identityCardService) diff --git a/presentation/user_route.go b/presentation/user_route.go index 86277eb..afe7d3e 100644 --- a/presentation/user_route.go +++ b/presentation/user_route.go @@ -11,19 +11,19 @@ import ( ) func UserProfileRouter(api fiber.Router) { - userProfileRepo := repositories.NewUserProfileRepository(config.DB) - userProfileService := services.NewUserProfileService(userProfileRepo) - userProfileHandler := handler.NewUserProfileHandler(userProfileService) + userProfileRepo := repositories.NewUserProfilRepository(config.DB) + userProfileService := services.NewUserService(userProfileRepo) + userProfileHandler := handler.NewUserHandler(userProfileService) userProfilRoute := api.Group("/user") - userProfilRoute.Get("/info", middleware.AuthMiddleware, userProfileHandler.GetUserProfile) + userProfilRoute.Get("/info", middleware.AuthMiddleware, userProfileHandler.GetUserByIDHandler) - userProfilRoute.Get("/show-all", middleware.AuthMiddleware, userProfileHandler.GetAllUsers) - userProfilRoute.Get("/:userid", middleware.AuthMiddleware, userProfileHandler.GetUserProfileById) - userProfilRoute.Get("/:roleid", middleware.AuthMiddleware, userProfileHandler.GetUsersByRoleID) + userProfilRoute.Get("/show-all", middleware.AuthMiddleware, userProfileHandler.GetAllUsersHandler) + // userProfilRoute.Get("/:userid", middleware.AuthMiddleware, userProfileHandler.GetUserProfileById) + // userProfilRoute.Get("/:roleid", middleware.AuthMiddleware, userProfileHandler.GetUsersByRoleID) - userProfilRoute.Put("/update-user", middleware.AuthMiddleware, userProfileHandler.UpdateUserProfile) - // userProfilRoute.Patch("/update-user-password", middleware.AuthMiddleware, userProfileHandler.UpdateUserPassword) - userProfilRoute.Patch("/upload-photoprofile", middleware.AuthMiddleware, userProfileHandler.UpdateUserAvatar) + userProfilRoute.Put("/update-user", middleware.AuthMiddleware, userProfileHandler.UpdateUserHandler) + userProfilRoute.Patch("/update-user-password", middleware.AuthMiddleware, userProfileHandler.UpdateUserPasswordHandler) + userProfilRoute.Patch("/upload-photoprofile", middleware.AuthMiddleware, userProfileHandler.UpdateUserAvatarHandler) }