diff --git a/app/module/about_us_content_images/service/about_us_content_images.service.go b/app/module/about_us_content_images/service/about_us_content_images.service.go index 0c1b680..22c21ce 100644 --- a/app/module/about_us_content_images/service/about_us_content_images.service.go +++ b/app/module/about_us_content_images/service/about_us_content_images.service.go @@ -2,21 +2,23 @@ package service import ( "fmt" + "mime" "mime/multipart" "path/filepath" "strings" - "time" + "web-qudo-be/app/database/entity" "web-qudo-be/app/module/about_us_content_images/repository" + minioStorage "web-qudo-be/config/config" + "web-qudo-be/utils/storage" "github.com/rs/zerolog" - - fileUtil "web-qudo-be/utils/file" ) type aboutUsContentImageService struct { - Repo repository.AboutUsContentImageRepository - Log zerolog.Logger + Repo repository.AboutUsContentImageRepository + MinioStorage *minioStorage.MinioStorage + Log zerolog.Logger } type AboutUsContentImageService interface { @@ -29,11 +31,13 @@ type AboutUsContentImageService interface { func NewAboutUsContentImageService( repo repository.AboutUsContentImageRepository, + minio *minioStorage.MinioStorage, log zerolog.Logger, ) AboutUsContentImageService { return &aboutUsContentImageService{ - Repo: repo, - Log: log, + Repo: repo, + MinioStorage: minio, + Log: log, } } @@ -50,34 +54,28 @@ func (_i *aboutUsContentImageService) Save(aboutUsContentId uint, file *multipar _i.Log.Info(). Uint("aboutUsContentId", aboutUsContentId). Str("filename", file.Filename). - Msg("upload image") + Msg("upload about us media") - ext := filepath.Ext(strings.ToLower(file.Filename)) - if ext != ".jpg" && ext != ".jpeg" && ext != ".png" && ext != ".mp4" && ext != ".webm" { - return nil, fmt.Errorf("invalid file type") - } - - // generate filename - filename := fmt.Sprintf("about_us_%d_%d%s", aboutUsContentId, time.Now().Unix(), ext) - - filePath := fmt.Sprintf("./uploads/%s", filename) - - // save file - if err := fileUtil.SaveFile(file, filePath); err != nil { - _i.Log.Error().Err(err).Msg("failed save file") + key, url, err := storage.UploadCMSObject(_i.MinioStorage, "about-us", file, true) + if err != nil { return nil, err } - // save ke DB - mt := "image/" + strings.TrimPrefix(ext, ".") - if ext == ".mp4" || ext == ".webm" { - mt = "video/" + strings.TrimPrefix(ext, ".") + ext := strings.ToLower(filepath.Ext(file.Filename)) + mt := mime.TypeByExtension(ext) + if mt == "" { + if ext == ".mp4" || ext == ".webm" { + mt = "video/" + strings.TrimPrefix(ext, ".") + } else { + mt = "application/octet-stream" + } } + data := &entity.AboutUsContentImage{ AboutUsContentID: aboutUsContentId, - MediaPath: filePath, + MediaPath: key, MediaType: mt, - MediaURL: "/uploads/" + filename, + MediaURL: url, } result, err := _i.Repo.Create(data) diff --git a/app/module/cms_media/cms_media.module.go b/app/module/cms_media/cms_media.module.go new file mode 100644 index 0000000..4a0d404 --- /dev/null +++ b/app/module/cms_media/cms_media.module.go @@ -0,0 +1,30 @@ +package cms_media + +import ( + "github.com/gofiber/fiber/v2" + "go.uber.org/fx" + + "web-qudo-be/app/module/cms_media/controller" + "web-qudo-be/app/module/cms_media/service" +) + +type CmsMediaRouter struct { + App *fiber.App + Ctrl *controller.CmsMediaController +} + +var NewCmsMediaModule = fx.Options( + fx.Provide(service.NewCmsMediaService), + fx.Provide(controller.NewCmsMediaController), + fx.Provide(NewCmsMediaRouter), +) + +func NewCmsMediaRouter(app *fiber.App, ctrl *controller.CmsMediaController) *CmsMediaRouter { + return &CmsMediaRouter{App: app, Ctrl: ctrl} +} + +func (r *CmsMediaRouter) RegisterCmsMediaRoutes() { + r.App.Route("/cms-media", func(router fiber.Router) { + router.Get("/viewer/*", r.Ctrl.Viewer) + }) +} diff --git a/app/module/cms_media/controller/cms_media.controller.go b/app/module/cms_media/controller/cms_media.controller.go new file mode 100644 index 0000000..124ca31 --- /dev/null +++ b/app/module/cms_media/controller/cms_media.controller.go @@ -0,0 +1,23 @@ +package controller + +import ( + "github.com/gofiber/fiber/v2" + "github.com/rs/zerolog" + + "web-qudo-be/app/module/cms_media/service" +) + +type CmsMediaController struct { + svc *service.CmsMediaService + Log zerolog.Logger +} + +func NewCmsMediaController(svc *service.CmsMediaService, log zerolog.Logger) *CmsMediaController { + return &CmsMediaController{svc: svc, Log: log} +} + +// Viewer streams CMS media from MinIO via API URL (for img/video src). +// @Router /cms-media/viewer/{path} [get] +func (ctrl *CmsMediaController) Viewer(c *fiber.Ctx) error { + return ctrl.svc.Viewer(c) +} diff --git a/app/module/cms_media/service/cms_media.service.go b/app/module/cms_media/service/cms_media.service.go new file mode 100644 index 0000000..d1a3f74 --- /dev/null +++ b/app/module/cms_media/service/cms_media.service.go @@ -0,0 +1,75 @@ +package service + +import ( + "context" + "io" + "mime" + "path/filepath" + "strings" + + "github.com/gofiber/fiber/v2" + "github.com/minio/minio-go/v7" + "github.com/rs/zerolog" + + minioStorage "web-qudo-be/config/config" +) + +type CmsMediaService struct { + Minio *minioStorage.MinioStorage + Log zerolog.Logger +} + +func NewCmsMediaService(minio *minioStorage.MinioStorage, log zerolog.Logger) *CmsMediaService { + return &CmsMediaService{Minio: minio, Log: log} +} + +// Viewer streams a CMS object from MinIO (same idea as article-files viewer). +func (s *CmsMediaService) Viewer(c *fiber.Ctx) error { + objectKey := strings.TrimSpace(c.Params("*")) + objectKey = strings.TrimPrefix(objectKey, "/") + if objectKey == "" { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "success": false, + "messages": []string{"object key required"}, + }) + } + if !strings.HasPrefix(objectKey, "cms/") || strings.Contains(objectKey, "..") { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "success": false, + "messages": []string{"invalid object key"}, + }) + } + + ctx := context.Background() + bucket := s.Minio.Cfg.ObjectStorage.MinioStorage.BucketName + + client, err := s.Minio.ConnectMinio() + if err != nil { + s.Log.Error().Err(err).Msg("cms media viewer: minio connect") + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "success": false, + "messages": []string{"storage unavailable"}, + }) + } + + obj, err := client.GetObject(ctx, bucket, objectKey, minio.GetObjectOptions{}) + if err != nil { + s.Log.Error().Err(err).Str("key", objectKey).Msg("cms media viewer: get object") + return c.Status(fiber.StatusNotFound).SendString("not found") + } + defer obj.Close() + + ext := strings.ToLower(filepath.Ext(objectKey)) + contentType := mime.TypeByExtension(ext) + if contentType == "" { + contentType = "application/octet-stream" + } + c.Set("Content-Type", contentType) + c.Set("Cache-Control", "public, max-age=86400") + + if _, err := io.Copy(c.Response().BodyWriter(), obj); err != nil { + s.Log.Error().Err(err).Msg("cms media viewer: stream") + return err + } + return nil +} diff --git a/app/module/hero_content_images/controller/hero_content_images.controller.go b/app/module/hero_content_images/controller/hero_content_images.controller.go index 4c1f0bc..19ede48 100644 --- a/app/module/hero_content_images/controller/hero_content_images.controller.go +++ b/app/module/hero_content_images/controller/hero_content_images.controller.go @@ -84,6 +84,21 @@ func (_i *heroContentImagesController) Update(c *fiber.Ctx) error { return err } + if strings.HasPrefix(c.Get("Content-Type"), "multipart/form-data") { + file, err := c.FormFile("file") + if err != nil { + return err + } + if err := _i.service.UpdateWithFile(id, file); err != nil { + _i.Log.Error().Err(err).Msg("failed update hero content image (upload)") + return err + } + return utilRes.Resp(c, utilRes.Response{ + Success: true, + Messages: utilRes.Messages{"Hero content image updated"}, + }) + } + req := new(request.HeroContentImageUpdateRequest) if err := utilVal.ParseAndValidate(c, req); err != nil { return err diff --git a/app/module/our_product_content_images/controller/our_product_content_images.controller.go b/app/module/our_product_content_images/controller/our_product_content_images.controller.go index b61cb0c..2230bd9 100644 --- a/app/module/our_product_content_images/controller/our_product_content_images.controller.go +++ b/app/module/our_product_content_images/controller/our_product_content_images.controller.go @@ -1,6 +1,8 @@ package controller import ( + "strings" + "github.com/gofiber/fiber/v2" "github.com/google/uuid" "github.com/rs/zerolog" @@ -53,6 +55,28 @@ func (_i *ourProductContentImagesController) FindByOurProductContentID(c *fiber. } func (_i *ourProductContentImagesController) Save(c *fiber.Ctx) error { + if strings.HasPrefix(c.Get("Content-Type"), "multipart/form-data") { + cidStr := c.FormValue("our_product_content_id") + cid, err := uuid.Parse(cidStr) + if err != nil { + return err + } + file, err := c.FormFile("file") + if err != nil { + return err + } + result, err := _i.service.SaveWithFile(cid, file) + if err != nil { + _i.Log.Error().Err(err).Msg("failed create our product content image (upload)") + return err + } + return utilRes.Resp(c, utilRes.Response{ + Success: true, + Messages: utilRes.Messages{"Our product content image created"}, + Data: result, + }) + } + req := new(request.OurProductContentImageCreateRequest) if err := utilVal.ParseAndValidate(c, req); err != nil { @@ -82,6 +106,21 @@ func (_i *ourProductContentImagesController) Update(c *fiber.Ctx) error { return err } + if strings.HasPrefix(c.Get("Content-Type"), "multipart/form-data") { + file, err := c.FormFile("file") + if err != nil { + return err + } + if err := _i.service.UpdateWithFile(id, file); err != nil { + _i.Log.Error().Err(err).Msg("failed update our product content image (upload)") + return err + } + return utilRes.Resp(c, utilRes.Response{ + Success: true, + Messages: utilRes.Messages{"Our product content image updated"}, + }) + } + req := new(request.OurProductContentImageUpdateRequest) if err := utilVal.ParseAndValidate(c, req); err != nil { return err diff --git a/app/module/our_product_content_images/service/our_product_content_images.service.go b/app/module/our_product_content_images/service/our_product_content_images.service.go index ab92635..666c4c0 100644 --- a/app/module/our_product_content_images/service/our_product_content_images.service.go +++ b/app/module/our_product_content_images/service/our_product_content_images.service.go @@ -1,32 +1,41 @@ package service import ( + "mime/multipart" + "github.com/google/uuid" "github.com/rs/zerolog" "web-qudo-be/app/database/entity" "web-qudo-be/app/module/our_product_content_images/repository" + minioStorage "web-qudo-be/config/config" + "web-qudo-be/utils/storage" ) type ourProductContentImagesService struct { - Repo repository.OurProductContentImagesRepository - Log zerolog.Logger + Repo repository.OurProductContentImagesRepository + MinioStorage *minioStorage.MinioStorage + Log zerolog.Logger } type OurProductContentImagesService interface { FindByContentID(contentID uuid.UUID) ([]entity.OurProductContentImage, error) Save(data *entity.OurProductContentImage) (*entity.OurProductContentImage, error) + SaveWithFile(ourProductContentID uuid.UUID, file *multipart.FileHeader) (*entity.OurProductContentImage, error) Update(id uuid.UUID, data *entity.OurProductContentImage) error + UpdateWithFile(id uuid.UUID, file *multipart.FileHeader) error Delete(id uuid.UUID) error } func NewOurProductContentImagesService( repo repository.OurProductContentImagesRepository, + minio *minioStorage.MinioStorage, log zerolog.Logger, ) OurProductContentImagesService { return &ourProductContentImagesService{ - Repo: repo, - Log: log, + Repo: repo, + MinioStorage: minio, + Log: log, } } @@ -52,6 +61,19 @@ func (s *ourProductContentImagesService) Save(data *entity.OurProductContentImag return result, nil } +func (s *ourProductContentImagesService) SaveWithFile(ourProductContentID uuid.UUID, file *multipart.FileHeader) (*entity.OurProductContentImage, error) { + key, url, err := storage.UploadCMSObject(s.MinioStorage, "our-products", file, false) + if err != nil { + return nil, err + } + data := &entity.OurProductContentImage{ + OurProductContentID: ourProductContentID, + ImagePath: key, + ImageURL: url, + } + return s.Save(data) +} + func (s *ourProductContentImagesService) Update(id uuid.UUID, data *entity.OurProductContentImage) error { err := s.Repo.Update(id, data) if err != nil { @@ -62,6 +84,17 @@ func (s *ourProductContentImagesService) Update(id uuid.UUID, data *entity.OurPr return nil } +func (s *ourProductContentImagesService) UpdateWithFile(id uuid.UUID, file *multipart.FileHeader) error { + key, url, err := storage.UploadCMSObject(s.MinioStorage, "our-products", file, false) + if err != nil { + return err + } + return s.Repo.Update(id, &entity.OurProductContentImage{ + ImagePath: key, + ImageURL: url, + }) +} + func (s *ourProductContentImagesService) Delete(id uuid.UUID) error { err := s.Repo.Delete(id) if err != nil { @@ -70,4 +103,4 @@ func (s *ourProductContentImagesService) Delete(id uuid.UUID) error { } return nil -} \ No newline at end of file +} diff --git a/app/module/our_service_content_images/controller/our_service_content_images.controller.go b/app/module/our_service_content_images/controller/our_service_content_images.controller.go index 54e66d5..5ba9bf4 100644 --- a/app/module/our_service_content_images/controller/our_service_content_images.controller.go +++ b/app/module/our_service_content_images/controller/our_service_content_images.controller.go @@ -2,6 +2,7 @@ package controller import ( "strconv" + "strings" "github.com/gofiber/fiber/v2" "github.com/rs/zerolog" @@ -56,6 +57,29 @@ func (_i *ourServiceContentImagesController) FindByOurServiceContentID(c *fiber. } func (_i *ourServiceContentImagesController) Save(c *fiber.Ctx) error { + if strings.HasPrefix(c.Get("Content-Type"), "multipart/form-data") { + cidStr := c.FormValue("our_service_content_id") + cidInt, err := strconv.Atoi(cidStr) + if err != nil { + return err + } + cid := uint(cidInt) + file, err := c.FormFile("file") + if err != nil { + return err + } + result, err := _i.service.SaveWithFile(cid, file) + if err != nil { + _i.Log.Error().Err(err).Msg("failed create our service content image (upload)") + return err + } + return utilRes.Resp(c, utilRes.Response{ + Success: true, + Messages: utilRes.Messages{"Our service content image created"}, + Data: result, + }) + } + req := new(request.OurServiceContentImageCreateRequest) if err := utilVal.ParseAndValidate(c, req); err != nil { @@ -87,6 +111,21 @@ func (_i *ourServiceContentImagesController) Update(c *fiber.Ctx) error { id := uint(idInt) + if strings.HasPrefix(c.Get("Content-Type"), "multipart/form-data") { + file, err := c.FormFile("file") + if err != nil { + return err + } + if err := _i.service.UpdateWithFile(id, file); err != nil { + _i.Log.Error().Err(err).Msg("failed update our service content image (upload)") + return err + } + return utilRes.Resp(c, utilRes.Response{ + Success: true, + Messages: utilRes.Messages{"Our service content image updated"}, + }) + } + req := new(request.OurServiceContentImageUpdateRequest) if err := utilVal.ParseAndValidate(c, req); err != nil { return err diff --git a/app/module/our_service_content_images/service/our_service_content_images.service.go b/app/module/our_service_content_images/service/our_service_content_images.service.go index 87e60b1..f5fcc73 100644 --- a/app/module/our_service_content_images/service/our_service_content_images.service.go +++ b/app/module/our_service_content_images/service/our_service_content_images.service.go @@ -1,31 +1,40 @@ package service import ( + "mime/multipart" + "github.com/rs/zerolog" "web-qudo-be/app/database/entity" "web-qudo-be/app/module/our_service_content_images/repository" + minioStorage "web-qudo-be/config/config" + "web-qudo-be/utils/storage" ) type ourServiceContentImagesService struct { - Repo repository.OurServiceContentImagesRepository - Log zerolog.Logger + Repo repository.OurServiceContentImagesRepository + MinioStorage *minioStorage.MinioStorage + Log zerolog.Logger } type OurServiceContentImagesService interface { FindByContentID(contentID uint) ([]entity.OurServiceContentImage, error) Save(data *entity.OurServiceContentImage) (*entity.OurServiceContentImage, error) + SaveWithFile(ourServiceContentID uint, file *multipart.FileHeader) (*entity.OurServiceContentImage, error) Update(id uint, data *entity.OurServiceContentImage) error + UpdateWithFile(id uint, file *multipart.FileHeader) error Delete(id uint) error } func NewOurServiceContentImagesService( repo repository.OurServiceContentImagesRepository, + minio *minioStorage.MinioStorage, log zerolog.Logger, ) OurServiceContentImagesService { return &ourServiceContentImagesService{ - Repo: repo, - Log: log, + Repo: repo, + MinioStorage: minio, + Log: log, } } @@ -49,6 +58,19 @@ func (s *ourServiceContentImagesService) Save(data *entity.OurServiceContentImag return result, nil } +func (s *ourServiceContentImagesService) SaveWithFile(ourServiceContentID uint, file *multipart.FileHeader) (*entity.OurServiceContentImage, error) { + key, url, err := storage.UploadCMSObject(s.MinioStorage, "our-services", file, false) + if err != nil { + return nil, err + } + data := &entity.OurServiceContentImage{ + OurServiceContentID: ourServiceContentID, + ImagePath: key, + ImageURL: url, + } + return s.Save(data) +} + func (s *ourServiceContentImagesService) Update(id uint, data *entity.OurServiceContentImage) error { err := s.Repo.Update(id, data) if err != nil { @@ -59,6 +81,17 @@ func (s *ourServiceContentImagesService) Update(id uint, data *entity.OurService return nil } +func (s *ourServiceContentImagesService) UpdateWithFile(id uint, file *multipart.FileHeader) error { + key, url, err := storage.UploadCMSObject(s.MinioStorage, "our-services", file, false) + if err != nil { + return err + } + return s.Repo.Update(id, &entity.OurServiceContentImage{ + ImagePath: key, + ImageURL: url, + }) +} + func (s *ourServiceContentImagesService) Delete(id uint) error { err := s.Repo.Delete(id) if err != nil { @@ -67,4 +100,4 @@ func (s *ourServiceContentImagesService) Delete(id uint) error { } return nil -} \ No newline at end of file +} diff --git a/app/module/partner_contents/controller/partner_contents.controller.go b/app/module/partner_contents/controller/partner_contents.controller.go index d3fe0d2..8cf6b40 100644 --- a/app/module/partner_contents/controller/partner_contents.controller.go +++ b/app/module/partner_contents/controller/partner_contents.controller.go @@ -21,6 +21,7 @@ type PartnerContentController interface { Show(c *fiber.Ctx) error Save(c *fiber.Ctx) error Update(c *fiber.Ctx) error + UploadLogo(c *fiber.Ctx) error Delete(c *fiber.Ctx) error } @@ -94,6 +95,26 @@ func (_i *partnerContentController) Update(c *fiber.Ctx) error { }) } +func (_i *partnerContentController) UploadLogo(c *fiber.Ctx) error { + idStr := c.Params("id") + id, err := uuid.Parse(idStr) + if err != nil { + return err + } + file, err := c.FormFile("file") + if err != nil { + return err + } + if err := _i.service.UploadLogo(id, file); err != nil { + _i.Log.Error().Err(err).Msg("failed upload partner logo") + return err + } + return utilRes.Resp(c, utilRes.Response{ + Success: true, + Messages: utilRes.Messages{"Partner logo uploaded"}, + }) +} + func (_i *partnerContentController) Delete(c *fiber.Ctx) error { idStr := c.Params("id") diff --git a/app/module/partner_contents/partner_contents.module.go b/app/module/partner_contents/partner_contents.module.go index 0312c7a..a404d21 100644 --- a/app/module/partner_contents/partner_contents.module.go +++ b/app/module/partner_contents/partner_contents.module.go @@ -42,6 +42,7 @@ func (_i *PartnerContentsRouter) RegisterPartnerContentsRoutes() { _i.App.Route("/partner-contents", func(router fiber.Router) { router.Get("/", partnerController.Show) router.Post("/", partnerController.Save) + router.Post("/:id/logo", partnerController.UploadLogo) router.Put("/:id", partnerController.Update) router.Delete("/:id", partnerController.Delete) }) diff --git a/app/module/partner_contents/repository/partner_contents.repository.go b/app/module/partner_contents/repository/partner_contents.repository.go index 0828bc2..ec2c958 100644 --- a/app/module/partner_contents/repository/partner_contents.repository.go +++ b/app/module/partner_contents/repository/partner_contents.repository.go @@ -17,6 +17,7 @@ type PartnerContentRepository interface { Get() ([]entity.PartnerContent, error) Create(data *entity.PartnerContent) (*entity.PartnerContent, error) Update(id uuid.UUID, data *entity.PartnerContent) error + UpdateImageFields(id uuid.UUID, imagePath, imageURL string) error Delete(id uuid.UUID) error FindByID(id uuid.UUID) (*entity.PartnerContent, error) // opsional (buat soft delete) } @@ -68,6 +69,21 @@ func (r *partnerContentRepository) Update(id uuid.UUID, data *entity.PartnerCont return nil } +func (r *partnerContentRepository) UpdateImageFields(id uuid.UUID, imagePath, imageURL string) error { + err := r.DB.DB. + Model(&entity.PartnerContent{}). + Where("id = ?", id). + Updates(map[string]interface{}{ + "image_path": imagePath, + "image_url": imageURL, + }).Error + if err != nil { + r.Log.Error().Err(err).Msg("failed update partner logo") + return err + } + return nil +} + func (r *partnerContentRepository) Delete(id uuid.UUID) error { err := r.DB.DB.Delete(&entity.PartnerContent{}, id).Error if err != nil { diff --git a/app/module/partner_contents/service/partner_contents.service.go b/app/module/partner_contents/service/partner_contents.service.go index 5535592..3e5e0af 100644 --- a/app/module/partner_contents/service/partner_contents.service.go +++ b/app/module/partner_contents/service/partner_contents.service.go @@ -1,32 +1,40 @@ package service import ( + "mime/multipart" + "github.com/google/uuid" "github.com/rs/zerolog" "web-qudo-be/app/database/entity" "web-qudo-be/app/module/partner_contents/repository" + minioStorage "web-qudo-be/config/config" + "web-qudo-be/utils/storage" ) type partnerContentService struct { - Repo repository.PartnerContentRepository - Log zerolog.Logger + Repo repository.PartnerContentRepository + MinioStorage *minioStorage.MinioStorage + Log zerolog.Logger } type PartnerContentService interface { Show() ([]entity.PartnerContent, error) Save(data *entity.PartnerContent) (*entity.PartnerContent, error) Update(id uuid.UUID, data *entity.PartnerContent) error + UploadLogo(id uuid.UUID, file *multipart.FileHeader) error Delete(id uuid.UUID) error } func NewPartnerContentService( repo repository.PartnerContentRepository, + minio *minioStorage.MinioStorage, log zerolog.Logger, ) PartnerContentService { return &partnerContentService{ - Repo: repo, - Log: log, + Repo: repo, + MinioStorage: minio, + Log: log, } } @@ -62,6 +70,14 @@ func (s *partnerContentService) Update(id uuid.UUID, data *entity.PartnerContent return nil } +func (s *partnerContentService) UploadLogo(id uuid.UUID, file *multipart.FileHeader) error { + key, url, err := storage.UploadCMSObject(s.MinioStorage, "partners", file, false) + if err != nil { + return err + } + return s.Repo.UpdateImageFields(id, key, url) +} + func (s *partnerContentService) Delete(id uuid.UUID) error { err := s.Repo.Delete(id) if err != nil { diff --git a/app/module/popup_news_content_images/controller/popup_news_content_images.controller.go b/app/module/popup_news_content_images/controller/popup_news_content_images.controller.go index c848450..136e415 100644 --- a/app/module/popup_news_content_images/controller/popup_news_content_images.controller.go +++ b/app/module/popup_news_content_images/controller/popup_news_content_images.controller.go @@ -2,6 +2,7 @@ package controller import ( "strconv" + "strings" "github.com/gofiber/fiber/v2" @@ -39,6 +40,32 @@ func NewPopupNewsContentImagesController(s service.PopupNewsContentImagesService // @Failure 500 {object} response.Response // @Router /popup-news-content-images [post] func (_i *popupNewsContentImagesController) Save(c *fiber.Ctx) error { + if strings.HasPrefix(c.Get("Content-Type"), "multipart/form-data") { + cidStr := c.FormValue("popup_news_content_id") + cid64, err := strconv.ParseUint(cidStr, 10, 32) + if err != nil { + return err + } + file, err := c.FormFile("file") + if err != nil { + return err + } + var isThumb *bool + if v := strings.TrimSpace(c.FormValue("is_thumbnail")); v != "" { + b := v == "true" || v == "1" || v == "on" + isThumb = &b + } + result, err := _i.service.SaveWithFile(uint(cid64), file, isThumb) + if err != nil { + return err + } + return utilRes.Resp(c, utilRes.Response{ + Success: true, + Messages: utilRes.Messages{"Popup news content image successfully uploaded"}, + Data: result, + }) + } + req := new(request.PopupNewsContentImagesCreateRequest) if err := utilVal.ParseAndValidate(c, req); err != nil { return err @@ -50,6 +77,7 @@ func (_i *popupNewsContentImagesController) Save(c *fiber.Ctx) error { } return utilRes.Resp(c, utilRes.Response{ + Success: true, Messages: utilRes.Messages{"Popup news content image successfully uploaded"}, }) } diff --git a/app/module/popup_news_content_images/service/popup_news_content_images.go b/app/module/popup_news_content_images/service/popup_news_content_images.go index 2629a5a..cb5a669 100644 --- a/app/module/popup_news_content_images/service/popup_news_content_images.go +++ b/app/module/popup_news_content_images/service/popup_news_content_images.go @@ -1,40 +1,76 @@ package service import ( + "mime/multipart" + "github.com/rs/zerolog" + "web-qudo-be/app/database/entity" "web-qudo-be/app/module/popup_news_content_images/repository" "web-qudo-be/app/module/popup_news_content_images/request" + minioStorage "web-qudo-be/config/config" + "web-qudo-be/utils/storage" ) -// service struct type popupNewsContentImagesService struct { - Repo repository.PopupNewsContentImagesRepository - Log zerolog.Logger + Repo repository.PopupNewsContentImagesRepository + MinioStorage *minioStorage.MinioStorage + Log zerolog.Logger } -// interface type PopupNewsContentImagesService interface { Save(req request.PopupNewsContentImagesCreateRequest) error + SaveWithFile(popupNewsContentID uint, file *multipart.FileHeader, isThumbnail *bool) (*entity.PopupNewsContentImages, error) Delete(id uint) error } -// constructor -func NewPopupNewsContentImagesService(repo repository.PopupNewsContentImagesRepository, log zerolog.Logger) PopupNewsContentImagesService { +func NewPopupNewsContentImagesService( + repo repository.PopupNewsContentImagesRepository, + minio *minioStorage.MinioStorage, + log zerolog.Logger, +) PopupNewsContentImagesService { return &popupNewsContentImagesService{ - Repo: repo, - Log: log, + Repo: repo, + MinioStorage: minio, + Log: log, } } -// Save func (_i *popupNewsContentImagesService) Save(req request.PopupNewsContentImagesCreateRequest) error { - _i.Log.Info().Interface("data", req).Msg("upload popup news content image") + _i.Log.Info().Interface("data", req).Msg("create popup news content image (json)") return _i.Repo.Create(req.ToEntity()) } -// Delete +func (_i *popupNewsContentImagesService) SaveWithFile(popupNewsContentID uint, file *multipart.FileHeader, isThumbnail *bool) (*entity.PopupNewsContentImages, error) { + _i.Log.Info(). + Uint("popup_news_content_id", popupNewsContentID). + Str("filename", file.Filename). + Msg("upload popup news content image") + + key, url, err := storage.UploadCMSObject(_i.MinioStorage, "popup-news", file, false) + if err != nil { + return nil, err + } + + if isThumbnail != nil && *isThumbnail { + if err := _i.Repo.ResetThumbnail(popupNewsContentID); err != nil { + return nil, err + } + } + + row := &entity.PopupNewsContentImages{ + PopupNewsContentID: popupNewsContentID, + MediaPath: key, + MediaURL: url, + IsThumbnail: isThumbnail, + } + if err := _i.Repo.Create(row); err != nil { + return nil, err + } + return row, nil +} + func (_i *popupNewsContentImagesService) Delete(id uint) error { return _i.Repo.Delete(id) -} \ No newline at end of file +} diff --git a/app/router/api.go b/app/router/api.go index 1c38c98..dd3e3ca 100644 --- a/app/router/api.go +++ b/app/router/api.go @@ -20,6 +20,7 @@ import ( "web-qudo-be/app/module/cities" "web-qudo-be/app/module/client_approval_settings" "web-qudo-be/app/module/clients" + "web-qudo-be/app/module/cms_media" "web-qudo-be/app/module/custom_static_pages" "web-qudo-be/app/module/districts" "web-qudo-be/app/module/feedbacks" @@ -72,7 +73,8 @@ type Router struct { CitiesRouter *cities.CitiesRouter ClientApprovalSettingsRouter *client_approval_settings.ClientApprovalSettingsRouter ClientsRouter *clients.ClientsRouter - HeroContentsRouter *hero_content.HeroContentsRouter + CmsMediaRouter *cms_media.CmsMediaRouter + HeroContentsRouter *hero_content.HeroContentsRouter HeroContentImagesRouter *hero_content_image.HeroContentImagesRouter CustomStaticPagesRouter *custom_static_pages.CustomStaticPagesRouter DistrictsRouter *districts.DistrictsRouter @@ -119,6 +121,7 @@ func NewRouter( citiesRouter *cities.CitiesRouter, clientApprovalSettingsRouter *client_approval_settings.ClientApprovalSettingsRouter, clientsRouter *clients.ClientsRouter, + cmsMediaRouter *cms_media.CmsMediaRouter, heroContentsRouter *hero_content.HeroContentsRouter, heroContentImagesRouter *hero_content_image.HeroContentImagesRouter, customStaticPagesRouter *custom_static_pages.CustomStaticPagesRouter, @@ -165,6 +168,7 @@ func NewRouter( CitiesRouter: citiesRouter, ClientApprovalSettingsRouter: clientApprovalSettingsRouter, ClientsRouter: clientsRouter, + CmsMediaRouter: cmsMediaRouter, HeroContentsRouter: heroContentsRouter, HeroContentImagesRouter: heroContentImagesRouter, CustomStaticPagesRouter: customStaticPagesRouter, @@ -221,6 +225,7 @@ func (r *Router) Register() { r.CitiesRouter.RegisterCitiesRoutes() r.ClientApprovalSettingsRouter.RegisterClientApprovalSettingsRoutes() r.ClientsRouter.RegisterClientsRoutes() + r.CmsMediaRouter.RegisterCmsMediaRoutes() r.HeroContentsRouter.RegisterHeroContentsRoutes() r.HeroContentImagesRouter.RegisterHeroContentImagesRoutes() r.CustomStaticPagesRouter.RegisterCustomStaticPagesRoutes() diff --git a/config/config/index.config.go b/config/config/index.config.go index 5f2d2e4..4d50bc8 100644 --- a/config/config/index.config.go +++ b/config/config/index.config.go @@ -128,6 +128,23 @@ type Config struct { Smtp smtp } +// APIPublicBaseURL is the base URL embedded in links returned to clients (e.g. CMS image preview). +// If app.production is true, it uses app.domain (trimmed). Otherwise it uses http://localhost plus app.port +// so local runs match opening the API in the browser without rewriting https://qudo.id/api. +func (c *Config) APIPublicBaseURL() string { + if c.App.Production { + return strings.TrimSuffix(c.App.Domain, "/") + } + port := strings.TrimSpace(c.App.Port) + if port == "" { + return "http://localhost" + } + if !strings.HasPrefix(port, ":") { + port = ":" + port + } + return "http://localhost" + port +} + // NewConfig : initialize config func NewConfig() *Config { config, err := ParseConfig("config") diff --git a/config/toml/config.toml b/config/toml/config.toml index 56c2843..7d6e3a2 100644 --- a/config/toml/config.toml +++ b/config/toml/config.toml @@ -8,6 +8,7 @@ external-port = ":8812" idle-timeout = 5 # As seconds print-routes = false prefork = false +# false: CMS preview URLs use http://localhost + port above. true: use domain (e.g. https://qudo.id/api). production = false body-limit = 1048576000 # "100 * 1024 * 1024" diff --git a/main.go b/main.go index 68c3207..d97db86 100644 --- a/main.go +++ b/main.go @@ -23,6 +23,7 @@ import ( "web-qudo-be/app/module/cities" "web-qudo-be/app/module/client_approval_settings" "web-qudo-be/app/module/clients" + "web-qudo-be/app/module/cms_media" "web-qudo-be/app/module/custom_static_pages" "web-qudo-be/app/module/districts" "web-qudo-be/app/module/feedbacks" @@ -104,6 +105,7 @@ func main() { cities.NewCitiesModule, client_approval_settings.NewClientApprovalSettingsModule, clients.NewClientsModule, + cms_media.NewCmsMediaModule, custom_static_pages.NewCustomStaticPagesModule, districts.NewDistrictsModule, feedbacks.NewFeedbacksModule, diff --git a/utils/storage/cms_upload.go b/utils/storage/cms_upload.go index 71dc9bf..df877e2 100644 --- a/utils/storage/cms_upload.go +++ b/utils/storage/cms_upload.go @@ -15,6 +15,13 @@ import ( appcfg "web-qudo-be/config/config" ) +// CMSPreviewURL is the absolute URL served by this API (GET /cms-media/viewer/...) for DB image_url / media_url fields. +func CMSPreviewURL(cfg *appcfg.Config, objectKey string) string { + base := cfg.APIPublicBaseURL() + key := strings.TrimPrefix(strings.TrimSpace(objectKey), "/") + return base + "/cms-media/viewer/" + key +} + var imageExts = map[string]bool{ ".jpg": true, ".jpeg": true, ".png": true, ".gif": true, ".webp": true, } @@ -24,8 +31,8 @@ var mediaExts = map[string]bool{ ".mp4": true, ".webm": true, } -// UploadCMSObject stores a file in MinIO under cms/{folder}/YYYY/MM/{uuid}{ext} and returns object key + public URL. -func UploadCMSObject(ms *appcfg.MinioStorage, folder string, file *multipart.FileHeader, allowVideo bool) (objectKey string, publicURL string, err error) { +// UploadCMSObject stores a file in MinIO under cms/{folder}/YYYY/MM/{uuid}{ext} and returns object key + preview URL (API viewer, not direct MinIO). +func UploadCMSObject(ms *appcfg.MinioStorage, folder string, file *multipart.FileHeader, allowVideo bool) (objectKey string, previewURL string, err error) { if file == nil { return "", "", fmt.Errorf("file is required") } @@ -65,5 +72,5 @@ func UploadCMSObject(ms *appcfg.MinioStorage, folder string, file *multipart.Fil return "", "", err } - return objectKey, ms.PublicObjectURL(objectKey), nil + return objectKey, CMSPreviewURL(ms.Cfg, objectKey), nil }