refact: optimize code
This commit is contained in:
parent
fc1298e894
commit
1f3c027cbd
|
@ -13,7 +13,7 @@ type LoginDTO struct {
|
||||||
|
|
||||||
type UserResponseWithToken struct {
|
type UserResponseWithToken struct {
|
||||||
UserID string `json:"user_id"`
|
UserID string `json:"user_id"`
|
||||||
RoleName string `json:"loginas"`
|
RoleName string `json:"role_name"`
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,39 +49,23 @@ func (l *LoginDTO) Validate() (map[string][]string, bool) {
|
||||||
func (r *RegisterDTO) Validate() (map[string][]string, bool) {
|
func (r *RegisterDTO) Validate() (map[string][]string, bool) {
|
||||||
errors := make(map[string][]string)
|
errors := make(map[string][]string)
|
||||||
|
|
||||||
if strings.TrimSpace(r.Username) == "" {
|
r.validateRequiredFields(errors)
|
||||||
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) == "" {
|
if r.Phone != "" && !IsValidPhoneNumber(r.Phone) {
|
||||||
errors["phone"] = append(errors["phone"], "Phone number is required")
|
|
||||||
} else if !IsValidPhoneNumber(r.Phone) {
|
|
||||||
errors["phone"] = append(errors["phone"], "Invalid phone number format. Use +62 followed by 9-13 digits")
|
errors["phone"] = append(errors["phone"], "Invalid phone number format. Use +62 followed by 9-13 digits")
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.TrimSpace(r.Email) == "" {
|
if r.Email != "" && !IsValidEmail(r.Email) {
|
||||||
errors["email"] = append(errors["email"], "Email is required")
|
|
||||||
} else if !IsValidEmail(r.Email) {
|
|
||||||
errors["email"] = append(errors["email"], "Invalid email format")
|
errors["email"] = append(errors["email"], "Invalid email format")
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.TrimSpace(r.Password) == "" {
|
if r.Password != "" && !IsValidPassword(r.Password) {
|
||||||
errors["password"] = append(errors["password"], "Password is required")
|
|
||||||
} else if !IsValidPassword(r.Password) {
|
|
||||||
errors["password"] = append(errors["password"], "Password must be at least 8 characters long and contain at least one number")
|
errors["password"] = append(errors["password"], "Password must be at least 8 characters long and contain at least one number")
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.TrimSpace(r.ConfirmPassword) == "" {
|
if r.ConfirmPassword != "" && r.Password != r.ConfirmPassword {
|
||||||
errors["confirm_password"] = append(errors["confirm_password"], "Confirm password is required")
|
|
||||||
} else if r.Password != r.ConfirmPassword {
|
|
||||||
errors["confirm_password"] = append(errors["confirm_password"], "Password and confirm password do not match")
|
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 {
|
if len(errors) > 0 {
|
||||||
return errors, false
|
return errors, false
|
||||||
|
@ -90,6 +74,31 @@ func (r *RegisterDTO) Validate() (map[string][]string, bool) {
|
||||||
return nil, true
|
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 {
|
func IsValidPhoneNumber(phone string) bool {
|
||||||
|
|
||||||
re := regexp.MustCompile(`^\+62\d{9,13}$`)
|
re := regexp.MustCompile(`^\+62\d{9,13}$`)
|
||||||
|
|
|
@ -18,8 +18,8 @@ func NewArticleHandler(articleService services.ArticleService) *ArticleHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *ArticleHandler) CreateArticle(c *fiber.Ctx) error {
|
func (h *ArticleHandler) CreateArticle(c *fiber.Ctx) error {
|
||||||
|
|
||||||
var request dto.RequestArticleDTO
|
var request dto.RequestArticleDTO
|
||||||
|
|
||||||
if err := c.BodyParser(&request); err != nil {
|
if err := c.BodyParser(&request); err != nil {
|
||||||
return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}})
|
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 {
|
func (h *ArticleHandler) GetAllArticles(c *fiber.Ctx) error {
|
||||||
|
|
||||||
page, err := strconv.Atoi(c.Query("page", "0"))
|
page, err := strconv.Atoi(c.Query("page", "0"))
|
||||||
if err != nil {
|
if err != nil || page < 1 {
|
||||||
page = 0
|
page = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
limit, err := strconv.Atoi(c.Query("limit", "0"))
|
limit, err := strconv.Atoi(c.Query("limit", "0"))
|
||||||
if err != nil {
|
if err != nil || limit < 1 {
|
||||||
limit = 0
|
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 {
|
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.NonPaginatedResponse(c, articles, totalArticles, "Articles fetched successfully")
|
||||||
return utils.PaginatedResponse(c, article, page, limit, totalArticle, "Article fetched successfully")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return utils.NonPaginatedResponse(c, article, totalArticle, "Article fetched successfully")
|
articles, totalArticles, err = h.ArticleService.GetAllArticles(page, limit)
|
||||||
|
if err != nil {
|
||||||
|
return utils.GenericErrorResponse(c, fiber.StatusInternalServerError, "Failed to fetch articles")
|
||||||
|
}
|
||||||
|
|
||||||
|
return utils.PaginatedResponse(c, articles, page, limit, totalArticles, "Articles fetched successfully")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *ArticleHandler) GetArticleByID(c *fiber.Ctx) error {
|
func (h *ArticleHandler) GetArticleByID(c *fiber.Ctx) error {
|
||||||
|
|
||||||
id := c.Params("article_id")
|
id := c.Params("article_id")
|
||||||
if id == "" {
|
if id == "" {
|
||||||
return utils.GenericErrorResponse(c, fiber.StatusBadRequest, "Article ID is required")
|
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")
|
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")
|
||||||
|
}
|
||||||
|
|
|
@ -37,9 +37,10 @@ func (h *UserHandler) Login(c *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *UserHandler) Register(c *fiber.Ctx) error {
|
func (h *UserHandler) Register(c *fiber.Ctx) error {
|
||||||
|
|
||||||
var registerDTO dto.RegisterDTO
|
var registerDTO dto.RegisterDTO
|
||||||
if err := c.BodyParser(®isterDTO); err != nil {
|
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()
|
errors, valid := registerDTO.Validate()
|
||||||
|
@ -47,26 +48,11 @@ func (h *UserHandler) Register(c *fiber.Ctx) error {
|
||||||
return utils.ValidationErrorResponse(c, errors)
|
return utils.ValidationErrorResponse(c, errors)
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := h.UserService.Register(registerDTO)
|
userResponse, err := h.UserService.Register(registerDTO)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return utils.GenericErrorResponse(c, fiber.StatusConflict, err.Error())
|
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")
|
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.InternalServerErrorResponse(c, "Error logging out")
|
||||||
}
|
}
|
||||||
|
|
||||||
return utils.NonPaginatedResponse(c, nil, 0, "Logout successful")
|
return utils.SuccessResponse(c, nil, "Logout successful")
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ type ArticleRepository interface {
|
||||||
CreateArticle(article *model.Article) error
|
CreateArticle(article *model.Article) error
|
||||||
FindArticleByID(id string) (*model.Article, error)
|
FindArticleByID(id string) (*model.Article, error)
|
||||||
FindAllArticles(page, limit int) ([]model.Article, int, error)
|
FindAllArticles(page, limit int) ([]model.Article, int, error)
|
||||||
|
UpdateArticle(id string, article *model.Article) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type articleRepository struct {
|
type articleRepository struct {
|
||||||
|
@ -20,11 +21,7 @@ func NewArticleRepository(db *gorm.DB) ArticleRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *articleRepository) CreateArticle(article *model.Article) error {
|
func (r *articleRepository) CreateArticle(article *model.Article) error {
|
||||||
err := r.DB.Create(article).Error
|
return r.DB.Create(article).Error
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *articleRepository) FindArticleByID(id string) (*model.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
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
err := r.DB.Find(&articles).Error
|
err := r.DB.Find(&articles).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
|
@ -59,3 +57,7 @@ func (r *articleRepository) FindAllArticles(page, limit int) ([]model.Article, i
|
||||||
|
|
||||||
return articles, int(total), nil
|
return articles, int(total), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *articleRepository) UpdateArticle(id string, article *model.Article) error {
|
||||||
|
return r.DB.Save(article).Error
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package repositories
|
package repositories
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/pahmiudahgede/senggoldong/model"
|
"github.com/pahmiudahgede/senggoldong/model"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
@ -29,6 +31,9 @@ func (r *userRepository) FindByIdentifierAndRole(identifier, roleID string) (*mo
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if user.Role == nil {
|
||||||
|
return nil, fmt.Errorf("role not found for this user")
|
||||||
|
}
|
||||||
return &user, nil
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ type ArticleService interface {
|
||||||
CreateArticle(request dto.RequestArticleDTO, coverImage *multipart.FileHeader) (*dto.ArticleResponseDTO, error)
|
CreateArticle(request dto.RequestArticleDTO, coverImage *multipart.FileHeader) (*dto.ArticleResponseDTO, error)
|
||||||
GetAllArticles(page, limit int) ([]dto.ArticleResponseDTO, int, error)
|
GetAllArticles(page, limit int) ([]dto.ArticleResponseDTO, int, error)
|
||||||
GetArticleByID(id string) (*dto.ArticleResponseDTO, error)
|
GetArticleByID(id string) (*dto.ArticleResponseDTO, error)
|
||||||
|
UpdateArticle(id string, request dto.RequestArticleDTO, coverImage *multipart.FileHeader) (*dto.ArticleResponseDTO, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type articleService struct {
|
type articleService struct {
|
||||||
|
@ -31,12 +32,9 @@ func NewArticleService(articleRepo repositories.ArticleRepository) ArticleServic
|
||||||
func (s *articleService) CreateArticle(request dto.RequestArticleDTO, coverImage *multipart.FileHeader) (*dto.ArticleResponseDTO, error) {
|
func (s *articleService) CreateArticle(request dto.RequestArticleDTO, coverImage *multipart.FileHeader) (*dto.ArticleResponseDTO, error) {
|
||||||
|
|
||||||
coverImageDir := "./public/uploads/articles"
|
coverImageDir := "./public/uploads/articles"
|
||||||
if _, err := os.Stat(coverImageDir); os.IsNotExist(err) {
|
if err := os.MkdirAll(coverImageDir, os.ModePerm); err != nil {
|
||||||
err := os.MkdirAll(coverImageDir, os.ModePerm)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create directory for cover image: %v", err)
|
return nil, fmt.Errorf("failed to create directory for cover image: %v", err)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
extension := filepath.Ext(coverImage.Filename)
|
extension := filepath.Ext(coverImage.Filename)
|
||||||
if extension != ".jpg" && extension != ".jpeg" && extension != ".png" {
|
if extension != ".jpg" && extension != ".jpeg" && extension != ".png" {
|
||||||
|
@ -58,8 +56,7 @@ func (s *articleService) CreateArticle(request dto.RequestArticleDTO, coverImage
|
||||||
}
|
}
|
||||||
defer dst.Close()
|
defer dst.Close()
|
||||||
|
|
||||||
_, err = dst.ReadFrom(src)
|
if _, err := dst.ReadFrom(src); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to save cover image: %v", err)
|
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,
|
Content: request.Content,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = s.ArticleRepo.CreateArticle(&article)
|
if err := s.ArticleRepo.CreateArticle(&article); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create article: %v", err)
|
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{}{
|
cacheData := map[string]interface{}{
|
||||||
"data": articleResponseDTO,
|
"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)
|
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
|
return articleResponseDTO, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,10 +143,12 @@ func (s *articleService) GetAllArticles(page, limit int) ([]dto.ArticleResponseD
|
||||||
cachedData, err := utils.GetJSONData(cacheKey)
|
cachedData, err := utils.GetJSONData(cacheKey)
|
||||||
if err == nil && cachedData != nil {
|
if err == nil && cachedData != nil {
|
||||||
var articles []dto.ArticleResponseDTO
|
var articles []dto.ArticleResponseDTO
|
||||||
|
|
||||||
if data, ok := cachedData["data"].([]interface{}); ok {
|
if data, ok := cachedData["data"].([]interface{}); ok {
|
||||||
for _, item := range data {
|
for _, item := range data {
|
||||||
articleData, ok := item.(map[string]interface{})
|
articleData, ok := item.(map[string]interface{})
|
||||||
if ok {
|
if ok {
|
||||||
|
|
||||||
articles = append(articles, dto.ArticleResponseDTO{
|
articles = append(articles, dto.ArticleResponseDTO{
|
||||||
ID: articleData["article_id"].(string),
|
ID: articleData["article_id"].(string),
|
||||||
Title: articleData["title"].(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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -153,6 +194,7 @@ func (s *articleService) GetAllArticles(page, limit int) ([]dto.ArticleResponseD
|
||||||
|
|
||||||
cacheData := map[string]interface{}{
|
cacheData := map[string]interface{}{
|
||||||
"data": articleDTOs,
|
"data": articleDTOs,
|
||||||
|
"total": total,
|
||||||
}
|
}
|
||||||
err = utils.SetJSONData(cacheKey, cacheData, time.Hour*24)
|
err = utils.SetJSONData(cacheKey, cacheData, time.Hour*24)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -169,6 +211,7 @@ func (s *articleService) GetArticleByID(id string) (*dto.ArticleResponseDTO, err
|
||||||
if err == nil && cachedData != nil {
|
if err == nil && cachedData != nil {
|
||||||
articleData, ok := cachedData["data"].(map[string]interface{})
|
articleData, ok := cachedData["data"].(map[string]interface{})
|
||||||
if ok {
|
if ok {
|
||||||
|
|
||||||
article := dto.ArticleResponseDTO{
|
article := dto.ArticleResponseDTO{
|
||||||
ID: articleData["article_id"].(string),
|
ID: articleData["article_id"].(string),
|
||||||
Title: articleData["title"].(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)
|
err = utils.SetJSONData(cacheKey, cacheData, time.Hour*24)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
||||||
fmt.Printf("Error caching article to Redis: %v\n", err)
|
fmt.Printf("Error caching article to Redis: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return articleResponseDTO, nil
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -13,9 +13,22 @@ import (
|
||||||
"golang.org/x/crypto/bcrypt"
|
"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 {
|
type UserService interface {
|
||||||
Login(credentials dto.LoginDTO) (*dto.UserResponseWithToken, error)
|
Login(credentials dto.LoginDTO) (*dto.UserResponseWithToken, error)
|
||||||
Register(user dto.RegisterDTO) (*model.User, error)
|
Register(user dto.RegisterDTO) (*dto.UserResponseDTO, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type userService struct {
|
type userService struct {
|
||||||
|
@ -29,18 +42,17 @@ func NewUserService(userRepo repositories.UserRepository, roleRepo repositories.
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *userService) Login(credentials dto.LoginDTO) (*dto.UserResponseWithToken, error) {
|
func (s *userService) Login(credentials dto.LoginDTO) (*dto.UserResponseWithToken, error) {
|
||||||
|
|
||||||
if credentials.RoleID == "" {
|
if credentials.RoleID == "" {
|
||||||
return nil, errors.New("roleId is required")
|
return nil, errors.New(ErrRoleIDRequired)
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := s.UserRepo.FindByIdentifierAndRole(credentials.Identifier, credentials.RoleID)
|
user, err := s.UserRepo.FindByIdentifierAndRole(credentials.Identifier, credentials.RoleID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("akun dengan role tersebut belum terdaftar")
|
return nil, errors.New(ErrAccountNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !CheckPasswordHash(credentials.Password, user.Password) {
|
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)
|
token, err := s.generateJWT(user)
|
||||||
|
@ -89,39 +101,36 @@ func CheckPasswordHash(password, hashedPassword string) bool {
|
||||||
return err == nil
|
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 {
|
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 == "" {
|
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)
|
role, err := s.RoleRepo.FindByID(user.RoleID)
|
||||||
if err != nil {
|
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, _ := s.UserRepo.FindByUsername(user.Username); existingUser != nil {
|
||||||
if existingUser != nil {
|
return nil, fmt.Errorf("%s: %v", ErrUsernameTaken, nil)
|
||||||
return nil, fmt.Errorf("username is already taken")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
existingPhone, _ := s.UserRepo.FindByPhoneAndRole(user.Phone, user.RoleID)
|
if existingPhone, _ := s.UserRepo.FindByPhoneAndRole(user.Phone, user.RoleID); existingPhone != nil {
|
||||||
if existingPhone != nil {
|
return nil, fmt.Errorf("%s: %v", ErrPhoneTaken, nil)
|
||||||
return nil, fmt.Errorf("phone number is already used for this role")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
existingEmail, _ := s.UserRepo.FindByEmailAndRole(user.Email, user.RoleID)
|
if existingEmail, _ := s.UserRepo.FindByEmailAndRole(user.Email, user.RoleID); existingEmail != nil {
|
||||||
if existingEmail != nil {
|
return nil, fmt.Errorf("%s: %v", ErrEmailTaken, nil)
|
||||||
return nil, fmt.Errorf("email is already used for this role")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to hash password: %v", err)
|
return nil, fmt.Errorf("%s: %v", ErrFailedToHashPassword, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
newUser := model.User{
|
newUser := model.User{
|
||||||
|
@ -135,10 +144,28 @@ func (s *userService) Register(user dto.RegisterDTO) (*model.User, error) {
|
||||||
|
|
||||||
err = s.UserRepo.Create(&newUser)
|
err = s.UserRepo.Create(&newUser)
|
||||||
if err != nil {
|
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
|
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,
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -12,7 +12,7 @@ type User struct {
|
||||||
EmailVerified bool `gorm:"default:false" json:"emailVerified"`
|
EmailVerified bool `gorm:"default:false" json:"emailVerified"`
|
||||||
Password string `gorm:"not null" json:"password"`
|
Password string `gorm:"not null" json:"password"`
|
||||||
RoleID string `gorm:"not null" json:"roleId"`
|
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"`
|
CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"`
|
||||||
UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"`
|
UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ func ArticleRouter(api fiber.Router) {
|
||||||
articleAPI := api.Group("/article-rijik")
|
articleAPI := api.Group("/article-rijik")
|
||||||
|
|
||||||
articleAPI.Post("/create-article", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator), articleHandler.CreateArticle)
|
articleAPI.Post("/create-article", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator), articleHandler.CreateArticle)
|
||||||
articleAPI.Get("/view-article", middleware.AuthMiddleware, articleHandler.GetAllArticles)
|
articleAPI.Get("/view-article", articleHandler.GetAllArticles)
|
||||||
articleAPI.Get("/view-article/:article_id", middleware.AuthMiddleware, articleHandler.GetArticleByID)
|
articleAPI.Get("/view-article/:article_id", articleHandler.GetArticleByID)
|
||||||
|
articleAPI.Put("/update-article/:article_id", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator), articleHandler.UpdateArticle)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package utils
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -15,6 +16,7 @@ var ctx = context.Background()
|
||||||
const defaultExpiration = 1 * time.Hour
|
const defaultExpiration = 1 * time.Hour
|
||||||
|
|
||||||
func SetData[T any](key string, value T, expiration time.Duration) error {
|
func SetData[T any](key string, value T, expiration time.Duration) error {
|
||||||
|
|
||||||
if expiration == 0 {
|
if expiration == 0 {
|
||||||
expiration = defaultExpiration
|
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()
|
err = config.RedisClient.Set(ctx, key, jsonData, expiration).Err()
|
||||||
if err != nil {
|
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)
|
log.Printf("Data stored in Redis with key: %s", key)
|
||||||
|
@ -40,7 +42,7 @@ func GetData(key string) (string, error) {
|
||||||
return "", nil
|
return "", nil
|
||||||
} else if err != 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
|
return val, nil
|
||||||
}
|
}
|
||||||
|
@ -48,7 +50,7 @@ func GetData(key string) (string, error) {
|
||||||
func DeleteData(key string) error {
|
func DeleteData(key string) error {
|
||||||
err := config.RedisClient.Del(ctx, key).Err()
|
err := config.RedisClient.Del(ctx, key).Err()
|
||||||
if err != nil {
|
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)
|
log.Printf("Data deleted from Redis with key: %s", key)
|
||||||
return nil
|
return nil
|
||||||
|
@ -57,7 +59,7 @@ func DeleteData(key string) error {
|
||||||
func CheckKeyExists(key string) (bool, error) {
|
func CheckKeyExists(key string) (bool, error) {
|
||||||
val, err := config.RedisClient.Exists(ctx, key).Result()
|
val, err := config.RedisClient.Exists(ctx, key).Result()
|
||||||
if err != nil {
|
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
|
return val > 0, nil
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue