update
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
This commit is contained in:
commit
2f1cef4a2a
|
|
@ -11,8 +11,7 @@ type AboutUsContentImage struct {
|
||||||
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
|
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
|
||||||
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
|
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
|
||||||
|
|
||||||
// relation (optional tapi bagus)
|
AboutUsContent AboutUsContent `json:"-" gorm:"foreignKey:AboutUsContentID"`
|
||||||
AboutUsContent AboutUsContent `json:"about_us_content" gorm:"foreignKey:AboutUsContentID"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (AboutUsContentImage) TableName() string {
|
func (AboutUsContentImage) TableName() string {
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ type AboutUsContent struct {
|
||||||
IsActive *bool `json:"is_active" gorm:"type:bool;default:true"`
|
IsActive *bool `json:"is_active" gorm:"type:bool;default:true"`
|
||||||
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
|
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
|
||||||
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
|
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
|
||||||
|
|
||||||
|
Images []AboutUsContentImage `json:"images,omitempty" gorm:"foreignKey:AboutUsContentID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (AboutUsContent) TableName() string {
|
func (AboutUsContent) TableName() string {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmsContentSubmission stores pending Content Website changes until an approver applies them.
|
||||||
|
type CmsContentSubmission struct {
|
||||||
|
ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"`
|
||||||
|
ClientID uuid.UUID `json:"client_id" gorm:"type:uuid;index"`
|
||||||
|
Domain string `json:"domain" gorm:"size:32;index"`
|
||||||
|
Title string `json:"title" gorm:"size:512"`
|
||||||
|
Status string `json:"status" gorm:"size:24;index"` // pending | approved | rejected
|
||||||
|
Payload string `json:"payload" gorm:"type:text"` // JSON
|
||||||
|
SubmittedByID uint `json:"submitted_by_id" gorm:"index"`
|
||||||
|
ReviewedByID *uint `json:"reviewed_by_id"`
|
||||||
|
ReviewNote string `json:"review_note" gorm:"size:512"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (CmsContentSubmission) TableName() string {
|
||||||
|
return "cms_content_submissions"
|
||||||
|
}
|
||||||
|
|
@ -7,7 +7,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type HeroContents struct {
|
type HeroContents struct {
|
||||||
ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;default:uuid_generate_v4()"`
|
ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid"`
|
||||||
PrimaryTitle string `json:"primary_title" gorm:"type:varchar(255)"`
|
PrimaryTitle string `json:"primary_title" gorm:"type:varchar(255)"`
|
||||||
SecondaryTitle string `json:"secondary_title" gorm:"type:varchar(255)"`
|
SecondaryTitle string `json:"secondary_title" gorm:"type:varchar(255)"`
|
||||||
Description string `json:"description" gorm:"type:text"`
|
Description string `json:"description" gorm:"type:text"`
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type HeroContentImages struct {
|
type HeroContentImages struct {
|
||||||
ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;default:uuid_generate_v4()"`
|
ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid"`
|
||||||
HeroContentID uuid.UUID `json:"hero_content_id" gorm:"type:uuid;not null"`
|
HeroContentID uuid.UUID `json:"hero_content_id" gorm:"type:uuid;not null"`
|
||||||
ImagePath string `json:"image_path" gorm:"type:text"`
|
ImagePath string `json:"image_path" gorm:"type:text"`
|
||||||
ImageURL string `json:"image_url" gorm:"type:text"`
|
ImageURL string `json:"image_url" gorm:"type:text"`
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MediaLibraryItem stores metadata for a single logical media asset (one public URL).
|
||||||
|
// The file may also be referenced from article_files, CMS image tables, etc.
|
||||||
|
type MediaLibraryItem struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
|
||||||
|
PublicURL string `json:"public_url" gorm:"type:varchar(2048);not null;uniqueIndex:ux_media_library_public_url"`
|
||||||
|
ObjectKey *string `json:"object_key" gorm:"type:varchar(1024)"`
|
||||||
|
OriginalFilename *string `json:"original_filename" gorm:"type:varchar(512)"`
|
||||||
|
FileCategory string `json:"file_category" gorm:"type:varchar(32);not null;default:other"` // image, video, audio, document, other
|
||||||
|
SizeBytes *int64 `json:"size_bytes" gorm:"type:int8"`
|
||||||
|
SourceType string `json:"source_type" gorm:"type:varchar(64);not null"` // article_file, cms, upload
|
||||||
|
SourceLabel *string `json:"source_label" gorm:"type:varchar(255)"`
|
||||||
|
ArticleFileID *uint `json:"article_file_id" gorm:"type:int4"`
|
||||||
|
CreatedByID int `json:"created_by_id" gorm:"type:int4;default:0"`
|
||||||
|
ClientID *uuid.UUID `json:"client_id" gorm:"type:UUID"`
|
||||||
|
IsActive bool `json:"is_active" gorm:"type:bool;default:true"`
|
||||||
|
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
|
||||||
|
}
|
||||||
|
|
@ -5,7 +5,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type OurProductContentImage struct {
|
type OurProductContentImage struct {
|
||||||
ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;default:uuid_generate_v4()"`
|
ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid"`
|
||||||
OurProductContentID uuid.UUID `json:"our_product_content_id" gorm:"type:uuid"`
|
OurProductContentID uuid.UUID `json:"our_product_content_id" gorm:"type:uuid"`
|
||||||
ImagePath string `json:"image_path" gorm:"type:varchar(255)"`
|
ImagePath string `json:"image_path" gorm:"type:varchar(255)"`
|
||||||
ImageURL string `json:"image_url" gorm:"type:text"`
|
ImageURL string `json:"image_url" gorm:"type:text"`
|
||||||
|
|
|
||||||
|
|
@ -7,17 +7,18 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type OurProductContent struct {
|
type OurProductContent struct {
|
||||||
ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;default:uuid_generate_v4()"`
|
ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid"`
|
||||||
PrimaryTitle string `json:"primary_title" gorm:"type:varchar(255)"`
|
PrimaryTitle string `json:"primary_title" gorm:"type:varchar(255)"`
|
||||||
SecondaryTitle string `json:"secondary_title" gorm:"type:varchar(255)"`
|
SecondaryTitle string `json:"secondary_title" gorm:"type:varchar(255)"`
|
||||||
Description string `json:"description" gorm:"type:text"`
|
Description string `json:"description" gorm:"type:text"`
|
||||||
|
LinkURL string `json:"link_url" gorm:"type:text"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
IsActive *bool `json:"is_active" gorm:"default:true"`
|
IsActive *bool `json:"is_active" gorm:"default:true"`
|
||||||
|
|
||||||
Images []OurProductContentImage `json:"images" gorm:"foreignKey:OurProductContentID"`
|
Images []OurProductContentImage `json:"images" gorm:"foreignKey:OurProductContentID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (OurProductContent) TableName() string {
|
func (OurProductContent) TableName() string {
|
||||||
return "our_product_contents"
|
return "our_product_contents"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,16 +5,17 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type OurServiceContent struct {
|
type OurServiceContent struct {
|
||||||
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||||
PrimaryTitle string `json:"primary_title" gorm:"type:varchar(255)"`
|
PrimaryTitle string `json:"primary_title" gorm:"type:varchar(255)"`
|
||||||
SecondaryTitle string `json:"secondary_title" gorm:"type:varchar(255)"`
|
SecondaryTitle string `json:"secondary_title" gorm:"type:varchar(255)"`
|
||||||
Description string `json:"description" gorm:"type:text"`
|
Description string `json:"description" gorm:"type:text"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
LinkURL string `json:"link_url" gorm:"type:text"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
IsActive *bool `json:"is_active" gorm:"default:true"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
Images []OurServiceContentImage `json:"images" gorm:"foreignKey:OurServiceContentID"`
|
IsActive *bool `json:"is_active" gorm:"default:true"`
|
||||||
|
Images []OurServiceContentImage `json:"images" gorm:"foreignKey:OurServiceContentID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (OurServiceContent) TableName() string {
|
func (OurServiceContent) TableName() string {
|
||||||
return "our_service_contents"
|
return "our_service_contents"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type PartnerContent struct {
|
type PartnerContent struct {
|
||||||
ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;default:uuid_generate_v4()"`
|
ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid"`
|
||||||
PrimaryTitle string `json:"primary_title" gorm:"type:varchar(255)"`
|
PrimaryTitle string `json:"primary_title" gorm:"type:varchar(255)"`
|
||||||
ImagePath string `json:"image_path" gorm:"type:varchar(255)"`
|
ImagePath string `json:"image_path" gorm:"type:varchar(255)"`
|
||||||
ImageURL string `json:"image_url" gorm:"type:text"`
|
ImageURL string `json:"image_url" gorm:"type:text"`
|
||||||
|
|
|
||||||
|
|
@ -103,16 +103,18 @@ func Models() []interface{} {
|
||||||
entity.Bookmarks{},
|
entity.Bookmarks{},
|
||||||
entity.Cities{},
|
entity.Cities{},
|
||||||
entity.Clients{},
|
entity.Clients{},
|
||||||
|
entity.HeroContents{},
|
||||||
|
entity.HeroContentImages{},
|
||||||
|
entity.CmsContentSubmission{},
|
||||||
entity.ClientApprovalSettings{},
|
entity.ClientApprovalSettings{},
|
||||||
entity.CsrfTokenRecords{},
|
entity.CsrfTokenRecords{},
|
||||||
entity.CustomStaticPages{},
|
entity.CustomStaticPages{},
|
||||||
entity.Districts{},
|
entity.Districts{},
|
||||||
entity.Feedbacks{},
|
entity.Feedbacks{},
|
||||||
entity.HeroContents{},
|
|
||||||
entity.HeroContentImages{},
|
|
||||||
entity.ForgotPasswords{},
|
entity.ForgotPasswords{},
|
||||||
entity.Magazines{},
|
entity.Magazines{},
|
||||||
entity.MagazineFiles{},
|
entity.MagazineFiles{},
|
||||||
|
entity.MediaLibraryItem{},
|
||||||
entity.MasterMenus{},
|
entity.MasterMenus{},
|
||||||
entity.MasterModules{},
|
entity.MasterModules{},
|
||||||
entity.MasterStatuses{},
|
entity.MasterStatuses{},
|
||||||
|
|
@ -121,7 +123,11 @@ func Models() []interface{} {
|
||||||
entity.OneTimePasswords{},
|
entity.OneTimePasswords{},
|
||||||
entity.OurProductContent{},
|
entity.OurProductContent{},
|
||||||
entity.OurProductContentImage{},
|
entity.OurProductContentImage{},
|
||||||
|
entity.OurServiceContent{},
|
||||||
|
entity.OurServiceContentImage{},
|
||||||
entity.PartnerContent{},
|
entity.PartnerContent{},
|
||||||
|
entity.PopupNewsContents{},
|
||||||
|
entity.PopupNewsContentImages{},
|
||||||
entity.Subscription{},
|
entity.Subscription{},
|
||||||
entity.Schedules{},
|
entity.Schedules{},
|
||||||
entity.UserLevels{},
|
entity.UserLevels{},
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ func (_i *AboutUsContentImageRouter) RegisterAboutUsContentImageRoutes() {
|
||||||
_i.App.Route("/about-us-content-images", func(router fiber.Router) {
|
_i.App.Route("/about-us-content-images", func(router fiber.Router) {
|
||||||
|
|
||||||
router.Get("/", aboutUsContentImageController.All)
|
router.Get("/", aboutUsContentImageController.All)
|
||||||
|
router.Post("/url", aboutUsContentImageController.SaveRemote)
|
||||||
router.Get("/:id", aboutUsContentImageController.Show)
|
router.Get("/:id", aboutUsContentImageController.Show)
|
||||||
|
|
||||||
// upload image (pakai form-data)
|
// upload image (pakai form-data)
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,14 @@ package controller
|
||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"web-qudo-be/app/module/about_us_content_images/request"
|
||||||
"web-qudo-be/app/module/about_us_content_images/service"
|
"web-qudo-be/app/module/about_us_content_images/service"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
|
||||||
utilRes "web-qudo-be/utils/response"
|
utilRes "web-qudo-be/utils/response"
|
||||||
|
utilVal "web-qudo-be/utils/validator"
|
||||||
)
|
)
|
||||||
|
|
||||||
type aboutUsContentImageController struct {
|
type aboutUsContentImageController struct {
|
||||||
|
|
@ -20,6 +22,7 @@ type AboutUsContentImageController interface {
|
||||||
All(c *fiber.Ctx) error
|
All(c *fiber.Ctx) error
|
||||||
Show(c *fiber.Ctx) error
|
Show(c *fiber.Ctx) error
|
||||||
Save(c *fiber.Ctx) error
|
Save(c *fiber.Ctx) error
|
||||||
|
SaveRemote(c *fiber.Ctx) error
|
||||||
Delete(c *fiber.Ctx) error
|
Delete(c *fiber.Ctx) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -95,6 +98,24 @@ func (_i *aboutUsContentImageController) Save(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SaveRemote JSON: public URL for image or video (e.g. CDN .mp4)
|
||||||
|
func (_i *aboutUsContentImageController) SaveRemote(c *fiber.Ctx) error {
|
||||||
|
req := new(request.AboutUsContentImageRemoteRequest)
|
||||||
|
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
result, err := _i.service.SaveRemoteURL(req.AboutUsContentID, req.MediaURL, req.MediaType)
|
||||||
|
if err != nil {
|
||||||
|
_i.Log.Error().Err(err).Msg("failed save remote about us media")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"About us media URL saved"},
|
||||||
|
Data: result,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// DELETE
|
// DELETE
|
||||||
func (_i *aboutUsContentImageController) Delete(c *fiber.Ctx) error {
|
func (_i *aboutUsContentImageController) Delete(c *fiber.Ctx) error {
|
||||||
id, err := strconv.Atoi(c.Params("id"))
|
id, err := strconv.Atoi(c.Params("id"))
|
||||||
|
|
|
||||||
|
|
@ -35,9 +35,7 @@ func NewAboutUsContentImageRepository(db *database.Database, log zerolog.Logger)
|
||||||
|
|
||||||
// GET ALL
|
// GET ALL
|
||||||
func (_i *aboutUsContentImageRepository) GetAll() (images []*entity.AboutUsContentImage, err error) {
|
func (_i *aboutUsContentImageRepository) GetAll() (images []*entity.AboutUsContentImage, err error) {
|
||||||
err = _i.DB.DB.
|
err = _i.DB.DB.Find(&images).Error
|
||||||
Where("is_active = ?", true).
|
|
||||||
Find(&images).Error
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,7 +50,7 @@ func (_i *aboutUsContentImageRepository) FindOne(id uint) (image *entity.AboutUs
|
||||||
// GET BY ABOUT US CONTENT ID
|
// GET BY ABOUT US CONTENT ID
|
||||||
func (_i *aboutUsContentImageRepository) FindByContentID(contentID uint) (images []*entity.AboutUsContentImage, err error) {
|
func (_i *aboutUsContentImageRepository) FindByContentID(contentID uint) (images []*entity.AboutUsContentImage, err error) {
|
||||||
err = _i.DB.DB.
|
err = _i.DB.DB.
|
||||||
Where("about_us_content_id = ? AND is_active = ?", contentID, true).
|
Where("about_us_content_id = ?", contentID).
|
||||||
Find(&images).Error
|
Find(&images).Error
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package request
|
||||||
|
|
||||||
|
type AboutUsContentImageRemoteRequest struct {
|
||||||
|
AboutUsContentID uint `json:"about_us_content_id" validate:"required"`
|
||||||
|
MediaURL string `json:"media_url" validate:"required"`
|
||||||
|
MediaType string `json:"media_type"`
|
||||||
|
}
|
||||||
|
|
@ -2,36 +2,46 @@ package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"mime"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"strings"
|
||||||
|
|
||||||
"web-qudo-be/app/database/entity"
|
"web-qudo-be/app/database/entity"
|
||||||
"web-qudo-be/app/module/about_us_content_images/repository"
|
"web-qudo-be/app/module/about_us_content_images/repository"
|
||||||
|
medialib "web-qudo-be/app/module/media_library/service"
|
||||||
|
minioStorage "web-qudo-be/config/config"
|
||||||
|
"web-qudo-be/utils/storage"
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
|
||||||
fileUtil "web-qudo-be/utils/file"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type aboutUsContentImageService struct {
|
type aboutUsContentImageService struct {
|
||||||
Repo repository.AboutUsContentImageRepository
|
Repo repository.AboutUsContentImageRepository
|
||||||
Log zerolog.Logger
|
MinioStorage *minioStorage.MinioStorage
|
||||||
|
MediaLib medialib.MediaLibraryService
|
||||||
|
Log zerolog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
type AboutUsContentImageService interface {
|
type AboutUsContentImageService interface {
|
||||||
All() (images []*entity.AboutUsContentImage, err error)
|
All() (images []*entity.AboutUsContentImage, err error)
|
||||||
Show(id uint) (image *entity.AboutUsContentImage, err error)
|
Show(id uint) (image *entity.AboutUsContentImage, err error)
|
||||||
Save(aboutUsContentId uint, file *multipart.FileHeader) (image *entity.AboutUsContentImage, err error)
|
Save(aboutUsContentId uint, file *multipart.FileHeader) (image *entity.AboutUsContentImage, err error)
|
||||||
|
SaveRemoteURL(aboutUsContentID uint, mediaURL, mediaType string) (image *entity.AboutUsContentImage, err error)
|
||||||
Delete(id uint) error
|
Delete(id uint) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAboutUsContentImageService(
|
func NewAboutUsContentImageService(
|
||||||
repo repository.AboutUsContentImageRepository,
|
repo repository.AboutUsContentImageRepository,
|
||||||
|
minio *minioStorage.MinioStorage,
|
||||||
|
mediaLib medialib.MediaLibraryService,
|
||||||
log zerolog.Logger,
|
log zerolog.Logger,
|
||||||
) AboutUsContentImageService {
|
) AboutUsContentImageService {
|
||||||
return &aboutUsContentImageService{
|
return &aboutUsContentImageService{
|
||||||
Repo: repo,
|
Repo: repo,
|
||||||
Log: log,
|
MinioStorage: minio,
|
||||||
|
MediaLib: mediaLib,
|
||||||
|
Log: log,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -48,31 +58,28 @@ func (_i *aboutUsContentImageService) Save(aboutUsContentId uint, file *multipar
|
||||||
_i.Log.Info().
|
_i.Log.Info().
|
||||||
Uint("aboutUsContentId", aboutUsContentId).
|
Uint("aboutUsContentId", aboutUsContentId).
|
||||||
Str("filename", file.Filename).
|
Str("filename", file.Filename).
|
||||||
Msg("upload image")
|
Msg("upload about us media")
|
||||||
|
|
||||||
// validasi file
|
key, url, err := storage.UploadCMSObject(_i.MinioStorage, "about-us", file, true)
|
||||||
ext := filepath.Ext(file.Filename)
|
if err != nil {
|
||||||
if ext != ".jpg" && ext != ".jpeg" && ext != ".png" {
|
|
||||||
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")
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// save ke DB
|
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{
|
data := &entity.AboutUsContentImage{
|
||||||
AboutUsContentID: aboutUsContentId,
|
AboutUsContentID: aboutUsContentId,
|
||||||
MediaPath: filePath,
|
MediaPath: key,
|
||||||
MediaType: ext,
|
MediaType: mt,
|
||||||
MediaURL: "/uploads/" + filename,
|
MediaURL: url,
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := _i.Repo.Create(data)
|
result, err := _i.Repo.Create(data)
|
||||||
|
|
@ -80,10 +87,37 @@ func (_i *aboutUsContentImageService) Save(aboutUsContentId uint, file *multipar
|
||||||
_i.Log.Error().Err(err).Msg("failed save to DB")
|
_i.Log.Error().Err(err).Msg("failed save to DB")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if _i.MediaLib != nil {
|
||||||
|
_ = _i.MediaLib.RegisterCMSAsset(url, key, "about_us", file)
|
||||||
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (_i *aboutUsContentImageService) SaveRemoteURL(aboutUsContentID uint, mediaURL, mediaType string) (image *entity.AboutUsContentImage, err error) {
|
||||||
|
if strings.TrimSpace(mediaURL) == "" {
|
||||||
|
return nil, fmt.Errorf("media_url is required")
|
||||||
|
}
|
||||||
|
mt := mediaType
|
||||||
|
if mt == "" {
|
||||||
|
lower := strings.ToLower(mediaURL)
|
||||||
|
if strings.HasSuffix(lower, ".mp4") || strings.Contains(lower, "video") {
|
||||||
|
mt = "video/mp4"
|
||||||
|
} else {
|
||||||
|
mt = "image/url"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data := &entity.AboutUsContentImage{
|
||||||
|
AboutUsContentID: aboutUsContentID,
|
||||||
|
MediaURL: mediaURL,
|
||||||
|
MediaType: mt,
|
||||||
|
}
|
||||||
|
img, err := _i.Repo.Create(data)
|
||||||
|
if err == nil && _i.MediaLib != nil {
|
||||||
|
_ = _i.MediaLib.RegisterCMSAsset(mediaURL, "", "about_us_remote", nil)
|
||||||
|
}
|
||||||
|
return img, err
|
||||||
|
}
|
||||||
|
|
||||||
func (_i *aboutUsContentImageService) Delete(id uint) error {
|
func (_i *aboutUsContentImageService) Delete(id uint) error {
|
||||||
return _i.Repo.Delete(id)
|
return _i.Repo.Delete(id)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package controller
|
||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"web-qudo-be/app/module/about_us_contents/request"
|
||||||
"web-qudo-be/app/module/about_us_contents/service"
|
"web-qudo-be/app/module/about_us_contents/service"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
|
|
@ -71,13 +72,13 @@ func (_i *aboutUsContentController) Show(c *fiber.Ctx) error {
|
||||||
|
|
||||||
// CREATE
|
// CREATE
|
||||||
func (_i *aboutUsContentController) Save(c *fiber.Ctx) error {
|
func (_i *aboutUsContentController) Save(c *fiber.Ctx) error {
|
||||||
req := new(map[string]interface{})
|
req := new(request.AboutUsContentCreateRequest)
|
||||||
|
|
||||||
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := _i.service.Save(*req)
|
result, err := _i.service.Save(req.ToEntity())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_i.Log.Error().Err(err).Msg("failed create about us content")
|
_i.Log.Error().Err(err).Msg("failed create about us content")
|
||||||
return err
|
return err
|
||||||
|
|
@ -97,13 +98,13 @@ func (_i *aboutUsContentController) Update(c *fiber.Ctx) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
req := new(map[string]interface{})
|
req := new(request.AboutUsContentUpdateRequest)
|
||||||
|
|
||||||
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = _i.service.Update(uint(id), *req)
|
err = _i.service.Update(uint(id), req.ToEntity())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_i.Log.Error().Err(err).Msg("failed update about us content")
|
_i.Log.Error().Err(err).Msg("failed update about us content")
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,9 @@ func (r *aboutUsContentRepository) GetAll() ([]*entity.AboutUsContent, error) {
|
||||||
var results []*entity.AboutUsContent
|
var results []*entity.AboutUsContent
|
||||||
|
|
||||||
err := r.DB.DB.
|
err := r.DB.DB.
|
||||||
Where("is_active = ?", true).
|
Preload("Images").
|
||||||
|
Where("is_active IS NULL OR is_active = ?", true).
|
||||||
|
Order("id ASC").
|
||||||
Find(&results).Error
|
Find(&results).Error
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -69,12 +71,24 @@ func (r *aboutUsContentRepository) Create(data *entity.AboutUsContent) (*entity.
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UPDATE
|
// Update uses a column map so we never SET primary key / created_at to zero values (breaks FK children).
|
||||||
func (r *aboutUsContentRepository) Update(id uint, data *entity.AboutUsContent) error {
|
func (r *aboutUsContentRepository) Update(id uint, data *entity.AboutUsContent) error {
|
||||||
|
updates := map[string]interface{}{
|
||||||
|
"primary_title": data.PrimaryTitle,
|
||||||
|
"secondary_title": data.SecondaryTitle,
|
||||||
|
"description": data.Description,
|
||||||
|
"primary_cta": data.PrimaryCta,
|
||||||
|
"secondary_cta_text": data.SecondaryCtaText,
|
||||||
|
"updated_at": data.UpdatedAt,
|
||||||
|
}
|
||||||
|
if data.IsActive != nil {
|
||||||
|
updates["is_active"] = data.IsActive
|
||||||
|
}
|
||||||
|
|
||||||
err := r.DB.DB.
|
err := r.DB.DB.
|
||||||
Model(&entity.AboutUsContent{}).
|
Model(&entity.AboutUsContent{}).
|
||||||
Where("id = ?", id).
|
Where("id = ?", id).
|
||||||
Updates(data).Error
|
Updates(updates).Error
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.Log.Error().Err(err).Msg("failed update about us content")
|
r.Log.Error().Err(err).Msg("failed update about us content")
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
|
||||||
"web-qudo-be/app/database/entity"
|
"web-qudo-be/app/database/entity"
|
||||||
|
|
@ -15,8 +17,8 @@ type aboutUsContentService struct {
|
||||||
type AboutUsContentService interface {
|
type AboutUsContentService interface {
|
||||||
All() ([]*entity.AboutUsContent, error)
|
All() ([]*entity.AboutUsContent, error)
|
||||||
Show(id uint) (*entity.AboutUsContent, error)
|
Show(id uint) (*entity.AboutUsContent, error)
|
||||||
Save(data map[string]interface{}) (*entity.AboutUsContent, error)
|
Save(data *entity.AboutUsContent) (*entity.AboutUsContent, error)
|
||||||
Update(id uint, data map[string]interface{}) error
|
Update(id uint, data *entity.AboutUsContent) error
|
||||||
Delete(id uint) error
|
Delete(id uint) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,26 +55,8 @@ func (s *aboutUsContentService) Show(id uint) (*entity.AboutUsContent, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// CREATE
|
// CREATE
|
||||||
func (s *aboutUsContentService) Save(data map[string]interface{}) (*entity.AboutUsContent, error) {
|
func (s *aboutUsContentService) Save(data *entity.AboutUsContent) (*entity.AboutUsContent, error) {
|
||||||
entityData := &entity.AboutUsContent{}
|
result, err := s.Repo.Create(data)
|
||||||
|
|
||||||
if v, ok := data["primary_title"].(string); ok {
|
|
||||||
entityData.PrimaryTitle = v
|
|
||||||
}
|
|
||||||
if v, ok := data["secondary_title"].(string); ok {
|
|
||||||
entityData.SecondaryTitle = v
|
|
||||||
}
|
|
||||||
if v, ok := data["description"].(string); ok {
|
|
||||||
entityData.Description = v
|
|
||||||
}
|
|
||||||
if v, ok := data["primary_cta"].(string); ok {
|
|
||||||
entityData.PrimaryCta = v
|
|
||||||
}
|
|
||||||
if v, ok := data["secondary_cta_text"].(string); ok {
|
|
||||||
entityData.SecondaryCtaText = v
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := s.Repo.Create(entityData)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Error().Err(err).Msg("failed create about us content")
|
s.Log.Error().Err(err).Msg("failed create about us content")
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -82,26 +66,8 @@ func (s *aboutUsContentService) Save(data map[string]interface{}) (*entity.About
|
||||||
}
|
}
|
||||||
|
|
||||||
// UPDATE
|
// UPDATE
|
||||||
func (s *aboutUsContentService) Update(id uint, data map[string]interface{}) error {
|
func (s *aboutUsContentService) Update(id uint, data *entity.AboutUsContent) error {
|
||||||
entityData := &entity.AboutUsContent{}
|
err := s.Repo.Update(id, data)
|
||||||
|
|
||||||
if v, ok := data["primary_title"].(string); ok {
|
|
||||||
entityData.PrimaryTitle = v
|
|
||||||
}
|
|
||||||
if v, ok := data["secondary_title"].(string); ok {
|
|
||||||
entityData.SecondaryTitle = v
|
|
||||||
}
|
|
||||||
if v, ok := data["description"].(string); ok {
|
|
||||||
entityData.Description = v
|
|
||||||
}
|
|
||||||
if v, ok := data["primary_cta"].(string); ok {
|
|
||||||
entityData.PrimaryCta = v
|
|
||||||
}
|
|
||||||
if v, ok := data["secondary_cta_text"].(string); ok {
|
|
||||||
entityData.SecondaryCtaText = v
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.Repo.Update(id, entityData)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Error().Err(err).Msg("failed update about us content")
|
s.Log.Error().Err(err).Msg("failed update about us content")
|
||||||
return err
|
return err
|
||||||
|
|
@ -119,6 +85,7 @@ func (s *aboutUsContentService) Delete(id uint) error {
|
||||||
|
|
||||||
isActive := false
|
isActive := false
|
||||||
result.IsActive = &isActive
|
result.IsActive = &isActive
|
||||||
|
result.UpdatedAt = time.Now()
|
||||||
|
|
||||||
return s.Repo.Update(id, result)
|
return s.Repo.Update(id, result)
|
||||||
}
|
}
|
||||||
|
|
@ -134,7 +134,11 @@ func (_i *articleFilesController) Save(c *fiber.Ctx) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = _i.articleFilesService.Save(clientId, c, uint(id))
|
uid := 0
|
||||||
|
if u := middleware.GetUser(c); u != nil {
|
||||||
|
uid = int(u.ID)
|
||||||
|
}
|
||||||
|
err = _i.articleFilesService.Save(clientId, c, uint(id), uid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import (
|
||||||
"web-qudo-be/app/module/article_files/repository"
|
"web-qudo-be/app/module/article_files/repository"
|
||||||
"web-qudo-be/app/module/article_files/request"
|
"web-qudo-be/app/module/article_files/request"
|
||||||
"web-qudo-be/app/module/article_files/response"
|
"web-qudo-be/app/module/article_files/response"
|
||||||
|
medialib "web-qudo-be/app/module/media_library/service"
|
||||||
config "web-qudo-be/config/config"
|
config "web-qudo-be/config/config"
|
||||||
"web-qudo-be/utils/paginator"
|
"web-qudo-be/utils/paginator"
|
||||||
|
|
||||||
|
|
@ -33,13 +34,14 @@ type articleFilesService struct {
|
||||||
Log zerolog.Logger
|
Log zerolog.Logger
|
||||||
Cfg *config.Config
|
Cfg *config.Config
|
||||||
MinioStorage *config.MinioStorage
|
MinioStorage *config.MinioStorage
|
||||||
|
MediaLib medialib.MediaLibraryService
|
||||||
}
|
}
|
||||||
|
|
||||||
// ArticleFilesService define interface of IArticleFilesService
|
// ArticleFilesService define interface of IArticleFilesService
|
||||||
type ArticleFilesService interface {
|
type ArticleFilesService interface {
|
||||||
All(clientId *uuid.UUID, req request.ArticleFilesQueryRequest) (articleFiles []*response.ArticleFilesResponse, paging paginator.Pagination, err error)
|
All(clientId *uuid.UUID, req request.ArticleFilesQueryRequest) (articleFiles []*response.ArticleFilesResponse, paging paginator.Pagination, err error)
|
||||||
Show(clientId *uuid.UUID, id uint) (articleFiles *response.ArticleFilesResponse, err error)
|
Show(clientId *uuid.UUID, id uint) (articleFiles *response.ArticleFilesResponse, err error)
|
||||||
Save(clientId *uuid.UUID, c *fiber.Ctx, id uint) error
|
Save(clientId *uuid.UUID, c *fiber.Ctx, articleID uint, createdByUserID int) error
|
||||||
SaveAsync(clientId *uuid.UUID, c *fiber.Ctx, id uint) error
|
SaveAsync(clientId *uuid.UUID, c *fiber.Ctx, id uint) error
|
||||||
Update(clientId *uuid.UUID, id uint, req request.ArticleFilesUpdateRequest) (err error)
|
Update(clientId *uuid.UUID, id uint, req request.ArticleFilesUpdateRequest) (err error)
|
||||||
GetUploadStatus(c *fiber.Ctx) (progress int, err error)
|
GetUploadStatus(c *fiber.Ctx) (progress int, err error)
|
||||||
|
|
@ -48,13 +50,14 @@ type ArticleFilesService interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewArticleFilesService init ArticleFilesService
|
// NewArticleFilesService init ArticleFilesService
|
||||||
func NewArticleFilesService(repo repository.ArticleFilesRepository, log zerolog.Logger, cfg *config.Config, minioStorage *config.MinioStorage) ArticleFilesService {
|
func NewArticleFilesService(repo repository.ArticleFilesRepository, log zerolog.Logger, cfg *config.Config, minioStorage *config.MinioStorage, mediaLib medialib.MediaLibraryService) ArticleFilesService {
|
||||||
|
|
||||||
return &articleFilesService{
|
return &articleFilesService{
|
||||||
Repo: repo,
|
Repo: repo,
|
||||||
Log: log,
|
Log: log,
|
||||||
Cfg: cfg,
|
Cfg: cfg,
|
||||||
MinioStorage: minioStorage,
|
MinioStorage: minioStorage,
|
||||||
|
MediaLib: mediaLib,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -195,7 +198,7 @@ func (_i *articleFilesService) SaveAsync(clientId *uuid.UUID, c *fiber.Ctx, id u
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (_i *articleFilesService) Save(clientId *uuid.UUID, c *fiber.Ctx, id uint) (err error) {
|
func (_i *articleFilesService) Save(clientId *uuid.UUID, c *fiber.Ctx, articleID uint, createdByUserID int) (err error) {
|
||||||
bucketName := _i.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName
|
bucketName := _i.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName
|
||||||
|
|
||||||
form, err := c.MultipartForm()
|
form, err := c.MultipartForm()
|
||||||
|
|
@ -250,14 +253,19 @@ func (_i *articleFilesService) Save(clientId *uuid.UUID, c *fiber.Ctx, id uint)
|
||||||
fileSize := strconv.FormatInt(fileHeader.Size, 10)
|
fileSize := strconv.FormatInt(fileHeader.Size, 10)
|
||||||
|
|
||||||
req := request.ArticleFilesCreateRequest{
|
req := request.ArticleFilesCreateRequest{
|
||||||
ArticleId: id,
|
ArticleId: articleID,
|
||||||
FilePath: &objectName,
|
FilePath: &objectName,
|
||||||
FileName: &newFilename,
|
FileName: &newFilename,
|
||||||
FileAlt: &filenameAlt,
|
FileAlt: &filenameAlt,
|
||||||
Size: &fileSize,
|
Size: &fileSize,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = _i.Repo.Create(clientId, req.ToEntity())
|
ent := req.ToEntity()
|
||||||
|
ent.CreatedById = createdByUserID
|
||||||
|
if ent.StatusId == 0 {
|
||||||
|
ent.StatusId = 1
|
||||||
|
}
|
||||||
|
err = _i.Repo.Create(clientId, ent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -267,6 +275,29 @@ func (_i *articleFilesService) Save(clientId *uuid.UUID, c *fiber.Ctx, id uint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if _i.MediaLib != nil && ent.FileName != nil {
|
||||||
|
pub := medialib.ArticleFilePublicURL(_i.Cfg, *ent.FileName)
|
||||||
|
var sizePtr *int64
|
||||||
|
if ent.Size != nil {
|
||||||
|
if n, perr := strconv.ParseInt(*ent.Size, 10, 64); perr == nil {
|
||||||
|
sizePtr = &n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lbl := fmt.Sprintf("article:%d", articleID)
|
||||||
|
_ = _i.MediaLib.UpsertRegister(medialib.RegisterInput{
|
||||||
|
ClientID: clientId,
|
||||||
|
UserID: createdByUserID,
|
||||||
|
PublicURL: pub,
|
||||||
|
ObjectKey: ent.FilePath,
|
||||||
|
OriginalFilename: ent.FileName,
|
||||||
|
FileCategory: medialib.CategoryFromFilename(*ent.FileName),
|
||||||
|
SizeBytes: sizePtr,
|
||||||
|
SourceType: "article_file",
|
||||||
|
SourceLabel: &lbl,
|
||||||
|
ArticleFileID: &ent.ID,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,8 @@ func (_i *articlesController) All(c *fiber.Ctx) error {
|
||||||
Source: c.Query("source"),
|
Source: c.Query("source"),
|
||||||
StartDate: c.Query("startDate"),
|
StartDate: c.Query("startDate"),
|
||||||
EndDate: c.Query("endDate"),
|
EndDate: c.Query("endDate"),
|
||||||
|
CreatedById: c.Query("createdById"),
|
||||||
|
MyContentMode: c.Query("myContentMode"),
|
||||||
}
|
}
|
||||||
req := reqContext.ToParamRequest()
|
req := reqContext.ToParamRequest()
|
||||||
req.Pagination = paginate
|
req.Pagination = paginate
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,15 @@ func (_i *articlesRepository) GetAll(clientId *uuid.UUID, userLevelId *uint, req
|
||||||
query = query.Where("articles.client_id = ?", clientId)
|
query = query.Where("articles.client_id = ?", clientId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if req.MyContentMode != nil {
|
||||||
|
mode := strings.ToLower(strings.TrimSpace(*req.MyContentMode))
|
||||||
|
if mode == "approver" {
|
||||||
|
query = query.Where("articles.is_draft = ?", false)
|
||||||
|
query = query.Joins("JOIN users acu ON acu.id = articles.created_by_id").
|
||||||
|
Where("acu.user_role_id = ?", 3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if req.Title != nil && *req.Title != "" {
|
if req.Title != nil && *req.Title != "" {
|
||||||
title := strings.ToLower(*req.Title)
|
title := strings.ToLower(*req.Title)
|
||||||
query = query.Where("LOWER(articles.title) LIKE ?", "%"+strings.ToLower(title)+"%")
|
query = query.Where("LOWER(articles.title) LIKE ?", "%"+strings.ToLower(title)+"%")
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package request
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"web-qudo-be/app/database/entity"
|
"web-qudo-be/app/database/entity"
|
||||||
"web-qudo-be/utils/paginator"
|
"web-qudo-be/utils/paginator"
|
||||||
|
|
@ -29,6 +30,8 @@ type ArticlesQueryRequest struct {
|
||||||
StartDate *time.Time `json:"startDate"`
|
StartDate *time.Time `json:"startDate"`
|
||||||
EndDate *time.Time `json:"endDate"`
|
EndDate *time.Time `json:"endDate"`
|
||||||
Pagination *paginator.Pagination `json:"pagination"`
|
Pagination *paginator.Pagination `json:"pagination"`
|
||||||
|
// myContentMode: "own" = current user's articles (any level); "approver" = non-draft from contributors (user_role_id 3) for approver history
|
||||||
|
MyContentMode *string `json:"myContentMode"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ArticlesCreateRequest struct {
|
type ArticlesCreateRequest struct {
|
||||||
|
|
@ -36,7 +39,7 @@ type ArticlesCreateRequest struct {
|
||||||
Slug string `json:"slug" validate:"required"`
|
Slug string `json:"slug" validate:"required"`
|
||||||
Description string `json:"description" validate:"required"`
|
Description string `json:"description" validate:"required"`
|
||||||
HtmlDescription string `json:"htmlDescription" validate:"required"`
|
HtmlDescription string `json:"htmlDescription" validate:"required"`
|
||||||
CategoryIds string `json:"categoryIds" validate:"required"`
|
CategoryIds string `json:"categoryIds" validate:"omitempty"`
|
||||||
TypeId int `json:"typeId" validate:"required"`
|
TypeId int `json:"typeId" validate:"required"`
|
||||||
Tags string `json:"tags" validate:"required"`
|
Tags string `json:"tags" validate:"required"`
|
||||||
AiArticleId *int `json:"aiArticleId"`
|
AiArticleId *int `json:"aiArticleId"`
|
||||||
|
|
@ -71,7 +74,7 @@ type ArticlesUpdateRequest struct {
|
||||||
Slug string `json:"slug" validate:"required"`
|
Slug string `json:"slug" validate:"required"`
|
||||||
Description string `json:"description" validate:"required"`
|
Description string `json:"description" validate:"required"`
|
||||||
HtmlDescription string `json:"htmlDescription" validate:"required"`
|
HtmlDescription string `json:"htmlDescription" validate:"required"`
|
||||||
CategoryIds string `json:"categoryIds" validate:"required"`
|
CategoryIds string `json:"categoryIds" validate:"omitempty"`
|
||||||
TypeId int `json:"typeId" validate:"required"`
|
TypeId int `json:"typeId" validate:"required"`
|
||||||
Tags string `json:"tags" validate:"required"`
|
Tags string `json:"tags" validate:"required"`
|
||||||
CreatedAt *string `json:"createdAt"`
|
CreatedAt *string `json:"createdAt"`
|
||||||
|
|
@ -137,6 +140,7 @@ type ArticlesQueryRequestContext struct {
|
||||||
CustomCreatorName string `json:"customCreatorName"`
|
CustomCreatorName string `json:"customCreatorName"`
|
||||||
StartDate string `json:"startDate"`
|
StartDate string `json:"startDate"`
|
||||||
EndDate string `json:"endDate"`
|
EndDate string `json:"endDate"`
|
||||||
|
MyContentMode string `json:"myContentMode"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (req ArticlesQueryRequestContext) ToParamRequest() ArticlesQueryRequest {
|
func (req ArticlesQueryRequestContext) ToParamRequest() ArticlesQueryRequest {
|
||||||
|
|
@ -213,6 +217,9 @@ func (req ArticlesQueryRequestContext) ToParamRequest() ArticlesQueryRequest {
|
||||||
request.EndDate = &endDate
|
request.EndDate = &endDate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if m := strings.TrimSpace(req.MyContentMode); m != "" {
|
||||||
|
request.MyContentMode = &m
|
||||||
|
}
|
||||||
|
|
||||||
return request
|
return request
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -120,27 +120,57 @@ func NewArticlesService(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
userRoleAdmin = uint(1)
|
||||||
|
userRoleApprover = uint(2)
|
||||||
|
userRoleContributor = uint(3)
|
||||||
|
)
|
||||||
|
|
||||||
|
func canUseMyContentApproverMode(roleID uint) bool {
|
||||||
|
return roleID == userRoleApprover || roleID == userRoleAdmin
|
||||||
|
}
|
||||||
|
|
||||||
// All implement interface of ArticlesService
|
// All implement interface of ArticlesService
|
||||||
func (_i *articlesService) All(clientId *uuid.UUID, authToken string, req request.ArticlesQueryRequest) (articless []*response.ArticlesResponse, paging paginator.Pagination, err error) {
|
func (_i *articlesService) All(clientId *uuid.UUID, authToken string, req request.ArticlesQueryRequest) (articless []*response.ArticlesResponse, paging paginator.Pagination, err error) {
|
||||||
// Extract userLevelId from authToken
|
|
||||||
var userLevelId *uint
|
var userLevelId *uint
|
||||||
|
reqScoped := req
|
||||||
|
|
||||||
if authToken != "" {
|
if authToken != "" {
|
||||||
user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
|
user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
|
||||||
if user != nil {
|
if user != nil {
|
||||||
userLevelId = &user.UserLevelId
|
if req.MyContentMode != nil {
|
||||||
_i.Log.Info().Interface("userLevelId", userLevelId).Msg("Extracted userLevelId from auth token")
|
mode := strings.ToLower(strings.TrimSpace(*req.MyContentMode))
|
||||||
|
switch mode {
|
||||||
|
case "own":
|
||||||
|
cb := int(user.ID)
|
||||||
|
reqScoped.CreatedById = &cb
|
||||||
|
userLevelId = nil
|
||||||
|
_i.Log.Info().Uint("userId", user.ID).Msg("myContentMode=own: list own articles without level visibility filter")
|
||||||
|
case "approver":
|
||||||
|
if !canUseMyContentApproverMode(user.UserRoleId) {
|
||||||
|
return nil, paging, errors.New("myContentMode approver requires approver or admin role")
|
||||||
|
}
|
||||||
|
userLevelId = nil
|
||||||
|
_i.Log.Info().Msg("myContentMode=approver: list contributor non-draft articles")
|
||||||
|
default:
|
||||||
|
userLevelId = &user.UserLevelId
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
userLevelId = &user.UserLevelId
|
||||||
|
}
|
||||||
|
_i.Log.Info().Interface("userLevelId", userLevelId).Msg("Articles.All visibility")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Category != nil {
|
if reqScoped.Category != nil {
|
||||||
findCategory, err := _i.ArticleCategoriesRepo.FindOneBySlug(clientId, *req.Category)
|
findCategory, err := _i.ArticleCategoriesRepo.FindOneBySlug(clientId, *reqScoped.Category)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, paging, err
|
return nil, paging, err
|
||||||
}
|
}
|
||||||
req.CategoryId = &findCategory.ID
|
reqScoped.CategoryId = &findCategory.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
results, paging, err := _i.Repo.GetAll(clientId, userLevelId, req)
|
results, paging, err := _i.Repo.GetAll(clientId, userLevelId, reqScoped)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -336,14 +366,23 @@ func (_i *articlesService) Save(clientId *uuid.UUID, req request.ArticlesCreateR
|
||||||
}
|
}
|
||||||
|
|
||||||
var categoryIds []string
|
var categoryIds []string
|
||||||
if req.CategoryIds != "" {
|
if strings.TrimSpace(req.CategoryIds) != "" {
|
||||||
categoryIds = strings.Split(req.CategoryIds, ",")
|
for _, part := range strings.Split(req.CategoryIds, ",") {
|
||||||
|
part = strings.TrimSpace(part)
|
||||||
|
if part == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
categoryIds = append(categoryIds, part)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_i.Log.Info().Interface("categoryIds", categoryIds).Msg("")
|
_i.Log.Info().Interface("categoryIds", categoryIds).Msg("")
|
||||||
|
|
||||||
for _, categoryId := range categoryIds {
|
for _, categoryId := range categoryIds {
|
||||||
categoryIdInt, _ := strconv.Atoi(categoryId)
|
categoryIdInt, parseErr := strconv.Atoi(categoryId)
|
||||||
|
if parseErr != nil || categoryIdInt <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
_i.Log.Info().Interface("categoryIdUint", uint(categoryIdInt)).Msg("")
|
_i.Log.Info().Interface("categoryIdUint", uint(categoryIdInt)).Msg("")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
package cms_content_submissions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"web-qudo-be/app/middleware"
|
||||||
|
"web-qudo-be/app/module/cms_content_submissions/controller"
|
||||||
|
"web-qudo-be/app/module/cms_content_submissions/repository"
|
||||||
|
"web-qudo-be/app/module/cms_content_submissions/service"
|
||||||
|
usersRepo "web-qudo-be/app/module/users/repository"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"go.uber.org/fx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CmsContentSubmissionsRouter struct {
|
||||||
|
App fiber.Router
|
||||||
|
Controller *controller.Controller
|
||||||
|
UsersRepo usersRepo.UsersRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
var NewCmsContentSubmissionsModule = fx.Options(
|
||||||
|
fx.Provide(repository.NewCmsContentSubmissionsRepository),
|
||||||
|
fx.Provide(service.NewCmsContentSubmissionsService),
|
||||||
|
fx.Provide(controller.NewController),
|
||||||
|
fx.Provide(NewCmsContentSubmissionsRouter),
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewCmsContentSubmissionsRouter(
|
||||||
|
fiber *fiber.App,
|
||||||
|
ctrl *controller.Controller,
|
||||||
|
usersRepo usersRepo.UsersRepository,
|
||||||
|
) *CmsContentSubmissionsRouter {
|
||||||
|
return &CmsContentSubmissionsRouter{
|
||||||
|
App: fiber,
|
||||||
|
Controller: ctrl,
|
||||||
|
UsersRepo: usersRepo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *CmsContentSubmissionsRouter) RegisterCmsContentSubmissionsRoutes() {
|
||||||
|
h := _i.Controller.CmsContentSubmissions
|
||||||
|
_i.App.Route("/cms-content-submissions", func(router fiber.Router) {
|
||||||
|
router.Use(middleware.UserMiddleware(_i.UsersRepo))
|
||||||
|
router.Post("/", h.Submit)
|
||||||
|
router.Get("/", h.List)
|
||||||
|
router.Post("/:id/approve", h.Approve)
|
||||||
|
router.Post("/:id/reject", h.Reject)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"web-qudo-be/app/middleware"
|
||||||
|
"web-qudo-be/app/module/cms_content_submissions/request"
|
||||||
|
"web-qudo-be/app/module/cms_content_submissions/service"
|
||||||
|
"web-qudo-be/utils/paginator"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
|
||||||
|
utilRes "web-qudo-be/utils/response"
|
||||||
|
utilVal "web-qudo-be/utils/validator"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CmsContentSubmissionsController interface {
|
||||||
|
Submit(c *fiber.Ctx) error
|
||||||
|
List(c *fiber.Ctx) error
|
||||||
|
Approve(c *fiber.Ctx) error
|
||||||
|
Reject(c *fiber.Ctx) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type cmsContentSubmissionsController struct {
|
||||||
|
svc service.CmsContentSubmissionsService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCmsContentSubmissionsController(svc service.CmsContentSubmissionsService) CmsContentSubmissionsController {
|
||||||
|
return &cmsContentSubmissionsController{svc: svc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *cmsContentSubmissionsController) Submit(c *fiber.Ctx) error {
|
||||||
|
user := middleware.GetUser(c)
|
||||||
|
clientID := middleware.GetClientID(c)
|
||||||
|
if user == nil || clientID == nil {
|
||||||
|
return fiber.NewError(fiber.StatusUnauthorized, "Unauthorized")
|
||||||
|
}
|
||||||
|
req := new(request.SubmitCmsContentSubmissionRequest)
|
||||||
|
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
row, err := _i.svc.Submit(clientID, user, req)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"CMS submission saved"},
|
||||||
|
Data: row,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *cmsContentSubmissionsController) List(c *fiber.Ctx) error {
|
||||||
|
user := middleware.GetUser(c)
|
||||||
|
clientID := middleware.GetClientID(c)
|
||||||
|
if user == nil || clientID == nil {
|
||||||
|
return fiber.NewError(fiber.StatusUnauthorized, "Unauthorized")
|
||||||
|
}
|
||||||
|
p, err := paginator.Paginate(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
status := c.Query("status")
|
||||||
|
mineOnly := c.Query("mine") == "1" || c.Query("mine") == "true"
|
||||||
|
rows, paging, err := _i.svc.List(clientID, user, status, mineOnly, p)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"CMS submissions loaded"},
|
||||||
|
Data: rows,
|
||||||
|
Meta: paging,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *cmsContentSubmissionsController) Approve(c *fiber.Ctx) error {
|
||||||
|
user := middleware.GetUser(c)
|
||||||
|
clientID := middleware.GetClientID(c)
|
||||||
|
if user == nil || clientID == nil {
|
||||||
|
return fiber.NewError(fiber.StatusUnauthorized, "Unauthorized")
|
||||||
|
}
|
||||||
|
id, err := uuid.Parse(c.Params("id"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := _i.svc.Approve(clientID, user, id); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"CMS submission approved and applied"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *cmsContentSubmissionsController) Reject(c *fiber.Ctx) error {
|
||||||
|
user := middleware.GetUser(c)
|
||||||
|
clientID := middleware.GetClientID(c)
|
||||||
|
if user == nil || clientID == nil {
|
||||||
|
return fiber.NewError(fiber.StatusUnauthorized, "Unauthorized")
|
||||||
|
}
|
||||||
|
id, err := uuid.Parse(c.Params("id"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req := new(request.RejectCmsContentSubmissionRequest)
|
||||||
|
_ = c.BodyParser(req)
|
||||||
|
if err := _i.svc.Reject(clientID, user, id, req.Note); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"CMS submission rejected"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"web-qudo-be/app/module/cms_content_submissions/service"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Controller struct {
|
||||||
|
CmsContentSubmissions CmsContentSubmissionsController
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewController(
|
||||||
|
svc service.CmsContentSubmissionsService,
|
||||||
|
log zerolog.Logger,
|
||||||
|
) *Controller {
|
||||||
|
_ = log
|
||||||
|
return &Controller{
|
||||||
|
CmsContentSubmissions: NewCmsContentSubmissionsController(svc),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"web-qudo-be/app/database"
|
||||||
|
"web-qudo-be/app/database/entity"
|
||||||
|
"web-qudo-be/utils/paginator"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CmsContentSubmissionsRepository interface {
|
||||||
|
Create(row *entity.CmsContentSubmission) error
|
||||||
|
FindByID(clientID uuid.UUID, id uuid.UUID) (*entity.CmsContentSubmission, error)
|
||||||
|
List(clientID uuid.UUID, status string, submittedByID *uint, p *paginator.Pagination) ([]entity.CmsContentSubmission, *paginator.Pagination, error)
|
||||||
|
Update(row *entity.CmsContentSubmission) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type cmsContentSubmissionsRepository struct {
|
||||||
|
DB *database.Database
|
||||||
|
Log zerolog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCmsContentSubmissionsRepository(db *database.Database, log zerolog.Logger) CmsContentSubmissionsRepository {
|
||||||
|
return &cmsContentSubmissionsRepository{DB: db, Log: log}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *cmsContentSubmissionsRepository) Create(row *entity.CmsContentSubmission) error {
|
||||||
|
return _i.DB.DB.Create(row).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *cmsContentSubmissionsRepository) FindByID(clientID uuid.UUID, id uuid.UUID) (*entity.CmsContentSubmission, error) {
|
||||||
|
var row entity.CmsContentSubmission
|
||||||
|
err := _i.DB.DB.Where("client_id = ? AND id = ?", clientID, id).First(&row).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &row, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *cmsContentSubmissionsRepository) List(clientID uuid.UUID, status string, submittedByID *uint, p *paginator.Pagination) ([]entity.CmsContentSubmission, *paginator.Pagination, error) {
|
||||||
|
var rows []entity.CmsContentSubmission
|
||||||
|
var count int64
|
||||||
|
|
||||||
|
q := _i.DB.DB.Model(&entity.CmsContentSubmission{}).Where("client_id = ?", clientID)
|
||||||
|
st := strings.TrimSpace(strings.ToLower(status))
|
||||||
|
if st != "" && st != "all" {
|
||||||
|
q = q.Where("status = ?", strings.TrimSpace(status))
|
||||||
|
}
|
||||||
|
if submittedByID != nil {
|
||||||
|
q = q.Where("submitted_by_id = ?", *submittedByID)
|
||||||
|
}
|
||||||
|
if err := q.Count(&count).Error; err != nil {
|
||||||
|
return nil, p, err
|
||||||
|
}
|
||||||
|
p.Count = count
|
||||||
|
p = paginator.Paging(p)
|
||||||
|
order := "created_at DESC"
|
||||||
|
if p.SortBy != "" {
|
||||||
|
dir := "DESC"
|
||||||
|
if p.Sort == "asc" {
|
||||||
|
dir = "ASC"
|
||||||
|
}
|
||||||
|
order = p.SortBy + " " + dir
|
||||||
|
}
|
||||||
|
err := q.Order(order).Offset(p.Offset).Limit(p.Limit).Find(&rows).Error
|
||||||
|
return rows, p, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *cmsContentSubmissionsRepository) Update(row *entity.CmsContentSubmission) error {
|
||||||
|
return _i.DB.DB.Save(row).Error
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package request
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
|
type SubmitCmsContentSubmissionRequest struct {
|
||||||
|
Domain string `json:"domain" validate:"required"`
|
||||||
|
Title string `json:"title" validate:"required"`
|
||||||
|
Payload json.RawMessage `json:"payload" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RejectCmsContentSubmissionRequest struct {
|
||||||
|
Note string `json:"note"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
package response
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CmsContentSubmissionListItem struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Payload string `json:"payload"`
|
||||||
|
SubmittedByID uint `json:"submitted_by_id"`
|
||||||
|
SubmitterName string `json:"submitter_name"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,623 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"web-qudo-be/app/database"
|
||||||
|
"web-qudo-be/app/database/entity"
|
||||||
|
"web-qudo-be/app/database/entity/users"
|
||||||
|
aboutUsImageSvc "web-qudo-be/app/module/about_us_content_images/service"
|
||||||
|
aboutUsSvc "web-qudo-be/app/module/about_us_contents/service"
|
||||||
|
"web-qudo-be/app/module/cms_content_submissions/repository"
|
||||||
|
"web-qudo-be/app/module/cms_content_submissions/request"
|
||||||
|
"web-qudo-be/app/module/cms_content_submissions/response"
|
||||||
|
heroImageSvc "web-qudo-be/app/module/hero_content_images/service"
|
||||||
|
heroSvc "web-qudo-be/app/module/hero_contents/service"
|
||||||
|
ourProductImageSvc "web-qudo-be/app/module/our_product_content_images/service"
|
||||||
|
ourProductSvc "web-qudo-be/app/module/our_product_contents/service"
|
||||||
|
ourServiceImageSvc "web-qudo-be/app/module/our_service_content_images/service"
|
||||||
|
ourServiceSvc "web-qudo-be/app/module/our_service_contents/service"
|
||||||
|
partnerSvc "web-qudo-be/app/module/partner_contents/service"
|
||||||
|
popupImageReq "web-qudo-be/app/module/popup_news_content_images/request"
|
||||||
|
popupImageSvc "web-qudo-be/app/module/popup_news_content_images/service"
|
||||||
|
popupNewsReq "web-qudo-be/app/module/popup_news_contents/request"
|
||||||
|
popupSvc "web-qudo-be/app/module/popup_news_contents/service"
|
||||||
|
"web-qudo-be/utils/paginator"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
cmsSubmissionPending = "pending"
|
||||||
|
cmsSubmissionApproved = "approved"
|
||||||
|
cmsSubmissionRejected = "rejected"
|
||||||
|
userRoleAdmin = uint(1)
|
||||||
|
userRoleApprover = uint(2)
|
||||||
|
userRoleContributor = uint(3)
|
||||||
|
)
|
||||||
|
|
||||||
|
func canApproveCmsSubmissions(roleID uint) bool {
|
||||||
|
return roleID == userRoleApprover || roleID == userRoleAdmin
|
||||||
|
}
|
||||||
|
|
||||||
|
type CmsContentSubmissionsService interface {
|
||||||
|
Submit(clientID *uuid.UUID, user *users.Users, req *request.SubmitCmsContentSubmissionRequest) (*entity.CmsContentSubmission, error)
|
||||||
|
List(clientID *uuid.UUID, user *users.Users, status string, mineOnly bool, p *paginator.Pagination) ([]response.CmsContentSubmissionListItem, *paginator.Pagination, error)
|
||||||
|
Approve(clientID *uuid.UUID, user *users.Users, id uuid.UUID) error
|
||||||
|
Reject(clientID *uuid.UUID, user *users.Users, id uuid.UUID, note string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type cmsContentSubmissionsService struct {
|
||||||
|
Repo repository.CmsContentSubmissionsRepository
|
||||||
|
DB *database.Database
|
||||||
|
Hero heroSvc.HeroContentsService
|
||||||
|
HeroImg heroImageSvc.HeroContentImagesService
|
||||||
|
About aboutUsSvc.AboutUsContentService
|
||||||
|
AboutImg aboutUsImageSvc.AboutUsContentImageService
|
||||||
|
OurProduct ourProductSvc.OurProductContentService
|
||||||
|
OurProductImg ourProductImageSvc.OurProductContentImagesService
|
||||||
|
OurService ourServiceSvc.OurServiceContentService
|
||||||
|
OurServiceImg ourServiceImageSvc.OurServiceContentImagesService
|
||||||
|
Partner partnerSvc.PartnerContentService
|
||||||
|
Popup popupSvc.PopupNewsContentsService
|
||||||
|
PopupImg popupImageSvc.PopupNewsContentImagesService
|
||||||
|
Log zerolog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCmsContentSubmissionsService(
|
||||||
|
repo repository.CmsContentSubmissionsRepository,
|
||||||
|
db *database.Database,
|
||||||
|
hero heroSvc.HeroContentsService,
|
||||||
|
heroImg heroImageSvc.HeroContentImagesService,
|
||||||
|
about aboutUsSvc.AboutUsContentService,
|
||||||
|
aboutImg aboutUsImageSvc.AboutUsContentImageService,
|
||||||
|
ourProduct ourProductSvc.OurProductContentService,
|
||||||
|
ourProductImg ourProductImageSvc.OurProductContentImagesService,
|
||||||
|
ourService ourServiceSvc.OurServiceContentService,
|
||||||
|
ourServiceImg ourServiceImageSvc.OurServiceContentImagesService,
|
||||||
|
partner partnerSvc.PartnerContentService,
|
||||||
|
popup popupSvc.PopupNewsContentsService,
|
||||||
|
popupImg popupImageSvc.PopupNewsContentImagesService,
|
||||||
|
log zerolog.Logger,
|
||||||
|
) CmsContentSubmissionsService {
|
||||||
|
return &cmsContentSubmissionsService{
|
||||||
|
Repo: repo,
|
||||||
|
DB: db,
|
||||||
|
Hero: hero,
|
||||||
|
HeroImg: heroImg,
|
||||||
|
About: about,
|
||||||
|
AboutImg: aboutImg,
|
||||||
|
OurProduct: ourProduct,
|
||||||
|
OurProductImg: ourProductImg,
|
||||||
|
OurService: ourService,
|
||||||
|
OurServiceImg: ourServiceImg,
|
||||||
|
Partner: partner,
|
||||||
|
Popup: popup,
|
||||||
|
PopupImg: popupImg,
|
||||||
|
Log: log,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *cmsContentSubmissionsService) Submit(clientID *uuid.UUID, user *users.Users, req *request.SubmitCmsContentSubmissionRequest) (*entity.CmsContentSubmission, error) {
|
||||||
|
if clientID == nil || user == nil {
|
||||||
|
return nil, errors.New("unauthorized")
|
||||||
|
}
|
||||||
|
if user.UserRoleId != userRoleContributor {
|
||||||
|
return nil, errors.New("only contributor role can submit CMS drafts")
|
||||||
|
}
|
||||||
|
domain := strings.TrimSpace(strings.ToLower(req.Domain))
|
||||||
|
if domain == "" {
|
||||||
|
return nil, errors.New("domain is required")
|
||||||
|
}
|
||||||
|
title := strings.TrimSpace(req.Title)
|
||||||
|
if title == "" {
|
||||||
|
return nil, errors.New("title is required")
|
||||||
|
}
|
||||||
|
if len(req.Payload) == 0 || string(req.Payload) == "null" {
|
||||||
|
return nil, errors.New("payload is required")
|
||||||
|
}
|
||||||
|
row := &entity.CmsContentSubmission{
|
||||||
|
ID: uuid.New(),
|
||||||
|
ClientID: *clientID,
|
||||||
|
Domain: domain,
|
||||||
|
Title: title,
|
||||||
|
Status: cmsSubmissionPending,
|
||||||
|
Payload: string(req.Payload),
|
||||||
|
SubmittedByID: user.ID,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
if err := _i.Repo.Create(row); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return row, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *cmsContentSubmissionsService) List(clientID *uuid.UUID, user *users.Users, status string, mineOnly bool, p *paginator.Pagination) ([]response.CmsContentSubmissionListItem, *paginator.Pagination, error) {
|
||||||
|
if clientID == nil || user == nil {
|
||||||
|
return nil, p, errors.New("unauthorized")
|
||||||
|
}
|
||||||
|
st := strings.TrimSpace(strings.ToLower(status))
|
||||||
|
var submittedBy *uint
|
||||||
|
if mineOnly {
|
||||||
|
submittedBy = &user.ID
|
||||||
|
} else if user.UserRoleId == userRoleContributor {
|
||||||
|
submittedBy = &user.ID
|
||||||
|
}
|
||||||
|
statusArg := status
|
||||||
|
if st == "" {
|
||||||
|
statusArg = "all"
|
||||||
|
}
|
||||||
|
rows, paging, err := _i.Repo.List(*clientID, statusArg, submittedBy, p)
|
||||||
|
if err != nil {
|
||||||
|
return nil, paging, err
|
||||||
|
}
|
||||||
|
out := make([]response.CmsContentSubmissionListItem, 0, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
name := ""
|
||||||
|
var u users.Users
|
||||||
|
if err := _i.DB.DB.Select("fullname").Where("id = ?", row.SubmittedByID).First(&u).Error; err == nil {
|
||||||
|
name = u.Fullname
|
||||||
|
}
|
||||||
|
out = append(out, response.CmsContentSubmissionListItem{
|
||||||
|
ID: row.ID,
|
||||||
|
Domain: row.Domain,
|
||||||
|
Title: row.Title,
|
||||||
|
Status: row.Status,
|
||||||
|
Payload: row.Payload,
|
||||||
|
SubmittedByID: row.SubmittedByID,
|
||||||
|
SubmitterName: name,
|
||||||
|
CreatedAt: row.CreatedAt,
|
||||||
|
UpdatedAt: row.UpdatedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out, paging, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *cmsContentSubmissionsService) Approve(clientID *uuid.UUID, user *users.Users, id uuid.UUID) error {
|
||||||
|
if clientID == nil || user == nil {
|
||||||
|
return errors.New("unauthorized")
|
||||||
|
}
|
||||||
|
if !canApproveCmsSubmissions(user.UserRoleId) {
|
||||||
|
return errors.New("only approver or admin role can approve CMS submissions")
|
||||||
|
}
|
||||||
|
row, err := _i.Repo.FindByID(*clientID, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if row.Status != cmsSubmissionPending {
|
||||||
|
return errors.New("submission is not pending")
|
||||||
|
}
|
||||||
|
if err := _i.applyDomainPayload(row.Domain, row.Payload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
row.Status = cmsSubmissionApproved
|
||||||
|
row.ReviewedByID = &user.ID
|
||||||
|
row.ReviewNote = ""
|
||||||
|
row.UpdatedAt = now
|
||||||
|
return _i.Repo.Update(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *cmsContentSubmissionsService) Reject(clientID *uuid.UUID, user *users.Users, id uuid.UUID, note string) error {
|
||||||
|
if clientID == nil || user == nil {
|
||||||
|
return errors.New("unauthorized")
|
||||||
|
}
|
||||||
|
if !canApproveCmsSubmissions(user.UserRoleId) {
|
||||||
|
return errors.New("only approver or admin role can reject CMS submissions")
|
||||||
|
}
|
||||||
|
row, err := _i.Repo.FindByID(*clientID, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if row.Status != cmsSubmissionPending {
|
||||||
|
return errors.New("submission is not pending")
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
row.Status = cmsSubmissionRejected
|
||||||
|
row.ReviewedByID = &user.ID
|
||||||
|
row.ReviewNote = strings.TrimSpace(note)
|
||||||
|
row.UpdatedAt = now
|
||||||
|
return _i.Repo.Update(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *cmsContentSubmissionsService) applyDomainPayload(domain string, payloadJSON string) error {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(domain)) {
|
||||||
|
case "hero":
|
||||||
|
return _i.mergeHero([]byte(payloadJSON))
|
||||||
|
case "about":
|
||||||
|
return _i.mergeAbout([]byte(payloadJSON))
|
||||||
|
case "product":
|
||||||
|
return _i.mergeProduct([]byte(payloadJSON))
|
||||||
|
case "service":
|
||||||
|
return _i.mergeService([]byte(payloadJSON))
|
||||||
|
case "partner":
|
||||||
|
return _i.mergePartner([]byte(payloadJSON))
|
||||||
|
case "popup":
|
||||||
|
return _i.mergePopup([]byte(payloadJSON))
|
||||||
|
default:
|
||||||
|
return errors.New("unknown domain")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type heroPayload struct {
|
||||||
|
Action string `json:"action"`
|
||||||
|
HeroID string `json:"hero_id"`
|
||||||
|
HeroImageID string `json:"hero_image_id"`
|
||||||
|
PrimaryTitle string `json:"primary_title"`
|
||||||
|
SecondaryTitle string `json:"secondary_title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
PrimaryCta string `json:"primary_cta"`
|
||||||
|
SecondaryCtaText string `json:"secondary_cta_text"`
|
||||||
|
ImageURL string `json:"image_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *cmsContentSubmissionsService) mergeHero(raw []byte) error {
|
||||||
|
var p heroPayload
|
||||||
|
if err := json.Unmarshal(raw, &p); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if strings.EqualFold(p.Action, "delete") {
|
||||||
|
return errors.New("hero delete is not supported")
|
||||||
|
}
|
||||||
|
ent := &entity.HeroContents{
|
||||||
|
PrimaryTitle: p.PrimaryTitle,
|
||||||
|
SecondaryTitle: p.SecondaryTitle,
|
||||||
|
Description: p.Description,
|
||||||
|
PrimaryCta: p.PrimaryCta,
|
||||||
|
SecondaryCtaText: p.SecondaryCtaText,
|
||||||
|
}
|
||||||
|
var heroUUID uuid.UUID
|
||||||
|
if strings.TrimSpace(p.HeroID) == "" {
|
||||||
|
saved, err := _i.Hero.Save(ent)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
heroUUID = saved.ID
|
||||||
|
} else {
|
||||||
|
id, err := uuid.Parse(p.HeroID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
heroUUID = id
|
||||||
|
if err := _i.Hero.Update(heroUUID, ent); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgURL := strings.TrimSpace(p.ImageURL)
|
||||||
|
if imgURL == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(p.HeroImageID) != "" {
|
||||||
|
imgID, err := uuid.Parse(p.HeroImageID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return _i.HeroImg.Update(imgID, &entity.HeroContentImages{
|
||||||
|
ID: imgID,
|
||||||
|
HeroContentID: heroUUID,
|
||||||
|
ImageURL: imgURL,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_, err := _i.HeroImg.Save(&entity.HeroContentImages{
|
||||||
|
HeroContentID: heroUUID,
|
||||||
|
ImageURL: imgURL,
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type aboutPayload struct {
|
||||||
|
Action string `json:"action"`
|
||||||
|
AboutID *int `json:"about_id"`
|
||||||
|
AboutMediaImageID *int `json:"about_media_image_id"`
|
||||||
|
PrimaryTitle string `json:"primary_title"`
|
||||||
|
SecondaryTitle string `json:"secondary_title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
PrimaryCta string `json:"primary_cta"`
|
||||||
|
SecondaryCtaText string `json:"secondary_cta_text"`
|
||||||
|
MediaURL string `json:"media_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *cmsContentSubmissionsService) mergeAbout(raw []byte) error {
|
||||||
|
var p aboutPayload
|
||||||
|
if err := json.Unmarshal(raw, &p); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if strings.EqualFold(p.Action, "delete") {
|
||||||
|
return errors.New("about delete is not supported")
|
||||||
|
}
|
||||||
|
ent := &entity.AboutUsContent{
|
||||||
|
PrimaryTitle: p.PrimaryTitle,
|
||||||
|
SecondaryTitle: p.SecondaryTitle,
|
||||||
|
Description: p.Description,
|
||||||
|
PrimaryCta: p.PrimaryCta,
|
||||||
|
SecondaryCtaText: p.SecondaryCtaText,
|
||||||
|
}
|
||||||
|
var aboutID uint
|
||||||
|
if p.AboutID == nil || *p.AboutID == 0 {
|
||||||
|
saved, err := _i.About.Save(ent)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
aboutID = saved.ID
|
||||||
|
} else {
|
||||||
|
aboutID = uint(*p.AboutID)
|
||||||
|
if err := _i.About.Update(aboutID, ent); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mediaURL := strings.TrimSpace(p.MediaURL)
|
||||||
|
if mediaURL == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if p.AboutMediaImageID != nil && *p.AboutMediaImageID > 0 {
|
||||||
|
_ = _i.AboutImg.Delete(uint(*p.AboutMediaImageID))
|
||||||
|
}
|
||||||
|
_, err := _i.AboutImg.SaveRemoteURL(aboutID, mediaURL, "")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type productPayload struct {
|
||||||
|
Action string `json:"action"`
|
||||||
|
ProductID string `json:"product_id"`
|
||||||
|
ProductImageID string `json:"product_image_id"`
|
||||||
|
PrimaryTitle string `json:"primary_title"`
|
||||||
|
SecondaryTitle string `json:"secondary_title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
LinkURL string `json:"link_url"`
|
||||||
|
ImageURL string `json:"image_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *cmsContentSubmissionsService) mergeProduct(raw []byte) error {
|
||||||
|
var p productPayload
|
||||||
|
if err := json.Unmarshal(raw, &p); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if strings.EqualFold(p.Action, "delete") {
|
||||||
|
if strings.TrimSpace(p.ProductID) == "" {
|
||||||
|
return errors.New("product_id required for delete")
|
||||||
|
}
|
||||||
|
id, err := uuid.Parse(p.ProductID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Remove images first (FK from our_product_content_images → our_product_contents).
|
||||||
|
imgs, err := _i.OurProductImg.FindByContentID(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i := range imgs {
|
||||||
|
if err := _i.OurProductImg.Delete(imgs[i].ID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _i.OurProduct.Delete(id)
|
||||||
|
}
|
||||||
|
ent := &entity.OurProductContent{
|
||||||
|
PrimaryTitle: p.PrimaryTitle,
|
||||||
|
SecondaryTitle: p.SecondaryTitle,
|
||||||
|
Description: p.Description,
|
||||||
|
LinkURL: p.LinkURL,
|
||||||
|
}
|
||||||
|
var pid uuid.UUID
|
||||||
|
if strings.TrimSpace(p.ProductID) == "" {
|
||||||
|
saved, err := _i.OurProduct.Save(ent)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pid = saved.ID
|
||||||
|
} else {
|
||||||
|
id, err := uuid.Parse(p.ProductID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pid = id
|
||||||
|
if err := _i.OurProduct.Update(pid, ent); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgURL := strings.TrimSpace(p.ImageURL)
|
||||||
|
if imgURL == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(p.ProductImageID) != "" {
|
||||||
|
imgID, err := uuid.Parse(p.ProductImageID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return _i.OurProductImg.Update(imgID, &entity.OurProductContentImage{
|
||||||
|
ID: imgID,
|
||||||
|
OurProductContentID: pid,
|
||||||
|
ImageURL: imgURL,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_, err := _i.OurProductImg.Save(&entity.OurProductContentImage{
|
||||||
|
OurProductContentID: pid,
|
||||||
|
ImageURL: imgURL,
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type servicePayload struct {
|
||||||
|
Action string `json:"action"`
|
||||||
|
ServiceID *int `json:"service_id"`
|
||||||
|
ServiceImageID string `json:"service_image_id"`
|
||||||
|
PrimaryTitle string `json:"primary_title"`
|
||||||
|
SecondaryTitle string `json:"secondary_title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
LinkURL string `json:"link_url"`
|
||||||
|
ImageURL string `json:"image_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *cmsContentSubmissionsService) mergeService(raw []byte) error {
|
||||||
|
var p servicePayload
|
||||||
|
if err := json.Unmarshal(raw, &p); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if strings.EqualFold(p.Action, "delete") {
|
||||||
|
if p.ServiceID == nil || *p.ServiceID == 0 {
|
||||||
|
return errors.New("service_id required for delete")
|
||||||
|
}
|
||||||
|
sid := uint(*p.ServiceID)
|
||||||
|
// Remove images first (FK from our_service_content_images → our_service_contents).
|
||||||
|
simgs, err := _i.OurServiceImg.FindByContentID(sid)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i := range simgs {
|
||||||
|
if err := _i.OurServiceImg.Delete(simgs[i].ID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _i.OurService.Delete(sid)
|
||||||
|
}
|
||||||
|
ent := &entity.OurServiceContent{
|
||||||
|
PrimaryTitle: p.PrimaryTitle,
|
||||||
|
SecondaryTitle: p.SecondaryTitle,
|
||||||
|
Description: p.Description,
|
||||||
|
LinkURL: p.LinkURL,
|
||||||
|
}
|
||||||
|
var sid uint
|
||||||
|
if p.ServiceID == nil || *p.ServiceID == 0 {
|
||||||
|
saved, err := _i.OurService.Save(ent)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sid = saved.ID
|
||||||
|
} else {
|
||||||
|
sid = uint(*p.ServiceID)
|
||||||
|
if err := _i.OurService.Update(sid, ent); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgURL := strings.TrimSpace(p.ImageURL)
|
||||||
|
if imgURL == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return _i.mergeServiceImage(sid, strings.TrimSpace(p.ServiceImageID), imgURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *cmsContentSubmissionsService) mergeServiceImage(sid uint, serviceImageID string, imgURL string) error {
|
||||||
|
if serviceImageID != "" {
|
||||||
|
n, err := strconv.ParseUint(serviceImageID, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
imgID := uint(n)
|
||||||
|
return _i.OurServiceImg.Update(imgID, &entity.OurServiceContentImage{
|
||||||
|
ID: imgID,
|
||||||
|
OurServiceContentID: sid,
|
||||||
|
ImageURL: imgURL,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_, err := _i.OurServiceImg.Save(&entity.OurServiceContentImage{
|
||||||
|
OurServiceContentID: sid,
|
||||||
|
ImageURL: imgURL,
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type partnerPayload struct {
|
||||||
|
Action string `json:"action"`
|
||||||
|
PartnerID string `json:"partner_id"`
|
||||||
|
PrimaryTitle string `json:"primary_title"`
|
||||||
|
ImagePath string `json:"image_path"`
|
||||||
|
ImageURL string `json:"image_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *cmsContentSubmissionsService) mergePartner(raw []byte) error {
|
||||||
|
var p partnerPayload
|
||||||
|
if err := json.Unmarshal(raw, &p); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if strings.EqualFold(p.Action, "delete") {
|
||||||
|
if strings.TrimSpace(p.PartnerID) == "" {
|
||||||
|
return errors.New("partner_id required for delete")
|
||||||
|
}
|
||||||
|
id, err := uuid.Parse(p.PartnerID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return _i.Partner.Delete(id)
|
||||||
|
}
|
||||||
|
ent := &entity.PartnerContent{
|
||||||
|
PrimaryTitle: strings.TrimSpace(p.PrimaryTitle),
|
||||||
|
ImagePath: p.ImagePath,
|
||||||
|
ImageURL: strings.TrimSpace(p.ImageURL),
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(p.PartnerID) == "" {
|
||||||
|
_, err := _i.Partner.Save(ent)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
id, err := uuid.Parse(p.PartnerID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return _i.Partner.Update(id, ent)
|
||||||
|
}
|
||||||
|
|
||||||
|
type popupPayload struct {
|
||||||
|
Action string `json:"action"`
|
||||||
|
PopupID *uint `json:"popup_id"`
|
||||||
|
PrimaryTitle string `json:"primary_title"`
|
||||||
|
SecondaryTitle string `json:"secondary_title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
PrimaryCta string `json:"primary_cta"`
|
||||||
|
SecondaryCtaText string `json:"secondary_cta_text"`
|
||||||
|
MediaURL string `json:"media_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *cmsContentSubmissionsService) mergePopup(raw []byte) error {
|
||||||
|
var p popupPayload
|
||||||
|
if err := json.Unmarshal(raw, &p); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if strings.EqualFold(p.Action, "delete") {
|
||||||
|
if p.PopupID == nil || *p.PopupID == 0 {
|
||||||
|
return errors.New("popup_id required for delete")
|
||||||
|
}
|
||||||
|
return _i.Popup.Delete(*p.PopupID)
|
||||||
|
}
|
||||||
|
if p.PopupID == nil || *p.PopupID == 0 {
|
||||||
|
res, err := _i.Popup.Save(popupNewsReq.PopupNewsContentsCreateRequest{
|
||||||
|
PrimaryTitle: p.PrimaryTitle,
|
||||||
|
SecondaryTitle: p.SecondaryTitle,
|
||||||
|
Description: p.Description,
|
||||||
|
PrimaryCTA: p.PrimaryCta,
|
||||||
|
SecondaryCTAText: p.SecondaryCtaText,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return _i.attachPopupImage(res.ID, p.MediaURL)
|
||||||
|
}
|
||||||
|
pid := *p.PopupID
|
||||||
|
if err := _i.Popup.Update(pid, popupNewsReq.PopupNewsContentsUpdateRequest{
|
||||||
|
ID: pid,
|
||||||
|
PrimaryTitle: p.PrimaryTitle,
|
||||||
|
SecondaryTitle: p.SecondaryTitle,
|
||||||
|
Description: p.Description,
|
||||||
|
PrimaryCTA: p.PrimaryCta,
|
||||||
|
SecondaryCTAText: p.SecondaryCtaText,
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return _i.attachPopupImage(pid, p.MediaURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *cmsContentSubmissionsService) attachPopupImage(popupID uint, mediaURL string) error {
|
||||||
|
mediaURL = strings.TrimSpace(mediaURL)
|
||||||
|
if mediaURL == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return _i.PopupImg.Save(popupImageReq.PopupNewsContentImagesCreateRequest{
|
||||||
|
PopupNewsContentID: popupID,
|
||||||
|
MediaPath: "",
|
||||||
|
MediaURL: mediaURL,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
|
@ -82,6 +84,21 @@ func (_i *heroContentImagesController) Update(c *fiber.Ctx) error {
|
||||||
return err
|
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)
|
req := new(request.HeroContentImageUpdateRequest)
|
||||||
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package repository
|
package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
"web-qudo-be/app/database"
|
"web-qudo-be/app/database"
|
||||||
"web-qudo-be/app/database/entity"
|
"web-qudo-be/app/database/entity"
|
||||||
|
|
||||||
|
|
@ -58,7 +59,11 @@ func (r *heroContentImagesRepository) Update(id uuid.UUID, data *entity.HeroCont
|
||||||
err := r.DB.DB.
|
err := r.DB.DB.
|
||||||
Model(&entity.HeroContentImages{}).
|
Model(&entity.HeroContentImages{}).
|
||||||
Where("id = ?", id).
|
Where("id = ?", id).
|
||||||
Updates(data).Error
|
Updates(map[string]interface{}{
|
||||||
|
"image_path": data.ImagePath,
|
||||||
|
"image_url": data.ImageURL,
|
||||||
|
"updated_at": time.Now(),
|
||||||
|
}).Error
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.Log.Error().Err(err).Msg("failed update hero content image")
|
r.Log.Error().Err(err).Msg("failed update hero content image")
|
||||||
|
|
@ -76,4 +81,4 @@ func (r *heroContentImagesRepository) Delete(id uuid.UUID) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,45 @@
|
||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"mime/multipart"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
|
||||||
"web-qudo-be/app/database/entity"
|
"web-qudo-be/app/database/entity"
|
||||||
"web-qudo-be/app/module/hero_content_images/repository"
|
"web-qudo-be/app/module/hero_content_images/repository"
|
||||||
|
medialib "web-qudo-be/app/module/media_library/service"
|
||||||
|
minioStorage "web-qudo-be/config/config"
|
||||||
|
"web-qudo-be/utils/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
type heroContentImagesService struct {
|
type heroContentImagesService struct {
|
||||||
Repo repository.HeroContentImagesRepository
|
Repo repository.HeroContentImagesRepository
|
||||||
Log zerolog.Logger
|
MinioStorage *minioStorage.MinioStorage
|
||||||
|
MediaLib medialib.MediaLibraryService
|
||||||
|
Log zerolog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
type HeroContentImagesService interface {
|
type HeroContentImagesService interface {
|
||||||
FindByHeroID(heroID uuid.UUID) (*entity.HeroContentImages, error)
|
FindByHeroID(heroID uuid.UUID) (*entity.HeroContentImages, error)
|
||||||
Save(data *entity.HeroContentImages) (*entity.HeroContentImages, error)
|
Save(data *entity.HeroContentImages) (*entity.HeroContentImages, error)
|
||||||
|
SaveWithFile(heroContentID uuid.UUID, file *multipart.FileHeader) (*entity.HeroContentImages, error)
|
||||||
Update(id uuid.UUID, data *entity.HeroContentImages) error
|
Update(id uuid.UUID, data *entity.HeroContentImages) error
|
||||||
|
UpdateWithFile(id uuid.UUID, file *multipart.FileHeader) error
|
||||||
Delete(id uuid.UUID) error
|
Delete(id uuid.UUID) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHeroContentImagesService(
|
func NewHeroContentImagesService(
|
||||||
repo repository.HeroContentImagesRepository,
|
repo repository.HeroContentImagesRepository,
|
||||||
|
minio *minioStorage.MinioStorage,
|
||||||
|
mediaLib medialib.MediaLibraryService,
|
||||||
log zerolog.Logger,
|
log zerolog.Logger,
|
||||||
) HeroContentImagesService {
|
) HeroContentImagesService {
|
||||||
return &heroContentImagesService{
|
return &heroContentImagesService{
|
||||||
Repo: repo,
|
Repo: repo,
|
||||||
Log: log,
|
MinioStorage: minio,
|
||||||
|
MediaLib: mediaLib,
|
||||||
|
Log: log,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,6 +65,26 @@ func (s *heroContentImagesService) Save(data *entity.HeroContentImages) (*entity
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *heroContentImagesService) SaveWithFile(heroContentID uuid.UUID, file *multipart.FileHeader) (*entity.HeroContentImages, error) {
|
||||||
|
key, url, err := storage.UploadCMSObject(s.MinioStorage, "hero", file, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
data := &entity.HeroContentImages{
|
||||||
|
HeroContentID: heroContentID,
|
||||||
|
ImagePath: key,
|
||||||
|
ImageURL: url,
|
||||||
|
}
|
||||||
|
out, err := s.Save(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if s.MediaLib != nil {
|
||||||
|
_ = s.MediaLib.RegisterCMSAsset(url, key, "hero_content", file)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *heroContentImagesService) Update(id uuid.UUID, data *entity.HeroContentImages) error {
|
func (s *heroContentImagesService) Update(id uuid.UUID, data *entity.HeroContentImages) error {
|
||||||
err := s.Repo.Update(id, data)
|
err := s.Repo.Update(id, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -62,6 +95,23 @@ func (s *heroContentImagesService) Update(id uuid.UUID, data *entity.HeroContent
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *heroContentImagesService) UpdateWithFile(id uuid.UUID, file *multipart.FileHeader) error {
|
||||||
|
key, url, err := storage.UploadCMSObject(s.MinioStorage, "hero", file, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.Repo.Update(id, &entity.HeroContentImages{
|
||||||
|
ImagePath: key,
|
||||||
|
ImageURL: url,
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if s.MediaLib != nil {
|
||||||
|
_ = s.MediaLib.RegisterCMSAsset(url, key, "hero_content", file)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *heroContentImagesService) Delete(id uuid.UUID) error {
|
func (s *heroContentImagesService) Delete(id uuid.UUID) error {
|
||||||
err := s.Repo.Delete(id)
|
err := s.Repo.Delete(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -70,4 +120,4 @@ func (s *heroContentImagesService) Delete(id uuid.UUID) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
package repository
|
package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
"web-qudo-be/app/database"
|
"web-qudo-be/app/database"
|
||||||
"web-qudo-be/app/database/entity"
|
"web-qudo-be/app/database/entity"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type heroContentsRepository struct {
|
type heroContentsRepository struct {
|
||||||
|
|
@ -35,6 +38,9 @@ func (r *heroContentsRepository) Get() (*entity.HeroContents, error) {
|
||||||
First(&data).Error
|
First(&data).Error
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
r.Log.Error().Err(err).Msg("failed get hero content")
|
r.Log.Error().Err(err).Msg("failed get hero content")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -55,10 +61,23 @@ func (r *heroContentsRepository) Create(data *entity.HeroContents) (*entity.Hero
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *heroContentsRepository) Update(id uuid.UUID, data *entity.HeroContents) error {
|
func (r *heroContentsRepository) Update(id uuid.UUID, data *entity.HeroContents) error {
|
||||||
|
// map (not struct) so empty strings are persisted; GORM Updates(struct) skips zero values.
|
||||||
|
updates := map[string]interface{}{
|
||||||
|
"primary_title": data.PrimaryTitle,
|
||||||
|
"secondary_title": data.SecondaryTitle,
|
||||||
|
"description": data.Description,
|
||||||
|
"primary_cta": data.PrimaryCta,
|
||||||
|
"secondary_cta_text": data.SecondaryCtaText,
|
||||||
|
}
|
||||||
|
if data.IsActive != nil {
|
||||||
|
updates["is_active"] = data.IsActive
|
||||||
|
}
|
||||||
|
updates["updated_at"] = time.Now()
|
||||||
|
|
||||||
err := r.DB.DB.
|
err := r.DB.DB.
|
||||||
Model(&entity.HeroContents{}).
|
Model(&entity.HeroContents{}).
|
||||||
Where("id = ?", id).
|
Where("id = ?", id).
|
||||||
Updates(data).Error
|
Updates(updates).Error
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.Log.Error().Err(err).Msg("failed update hero content")
|
r.Log.Error().Err(err).Msg("failed update hero content")
|
||||||
|
|
@ -76,4 +95,4 @@ func (r *heroContentsRepository) Delete(id uuid.UUID) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ func (r *HeroContentsCreateRequest) ToEntity() *entity.HeroContents {
|
||||||
}
|
}
|
||||||
|
|
||||||
type HeroContentsUpdateRequest struct {
|
type HeroContentsUpdateRequest struct {
|
||||||
PrimaryTitle string `json:"primary_title"`
|
PrimaryTitle string `json:"primary_title" validate:"required"`
|
||||||
SecondaryTitle string `json:"secondary_title"`
|
SecondaryTitle string `json:"secondary_title"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
PrimaryCTA string `json:"primary_cta"`
|
PrimaryCTA string `json:"primary_cta"`
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
|
||||||
|
"web-qudo-be/app/middleware"
|
||||||
|
"web-qudo-be/app/module/media_library/request"
|
||||||
|
"web-qudo-be/app/module/media_library/service"
|
||||||
|
"web-qudo-be/utils/paginator"
|
||||||
|
utilRes "web-qudo-be/utils/response"
|
||||||
|
utilVal "web-qudo-be/utils/validator"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MediaLibraryController struct {
|
||||||
|
svc service.MediaLibraryService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMediaLibraryController(svc service.MediaLibraryService) *MediaLibraryController {
|
||||||
|
return &MediaLibraryController{svc: svc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *MediaLibraryController) All(c *fiber.Ctx) error {
|
||||||
|
clientID := middleware.GetClientID(c)
|
||||||
|
user := middleware.GetUser(c)
|
||||||
|
uid := 0
|
||||||
|
if user != nil {
|
||||||
|
uid = int(user.ID)
|
||||||
|
}
|
||||||
|
_ = uid
|
||||||
|
|
||||||
|
paginate, err := paginator.Paginate(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
q := strings.TrimSpace(c.Query("q"))
|
||||||
|
var st *string
|
||||||
|
if v := strings.TrimSpace(c.Query("source_type")); v != "" {
|
||||||
|
st = &v
|
||||||
|
}
|
||||||
|
data, paging, err := _i.svc.All(clientID, q, st, paginate)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"Media library list"},
|
||||||
|
Data: data,
|
||||||
|
Meta: paging,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *MediaLibraryController) Register(c *fiber.Ctx) error {
|
||||||
|
clientID := middleware.GetClientID(c)
|
||||||
|
user := middleware.GetUser(c)
|
||||||
|
uid := 0
|
||||||
|
if user != nil {
|
||||||
|
uid = int(user.ID)
|
||||||
|
}
|
||||||
|
req := new(request.MediaLibraryRegisterRequest)
|
||||||
|
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := _i.svc.RegisterFromRequest(clientID, uid, req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"Registered to media library"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *MediaLibraryController) Upload(c *fiber.Ctx) error {
|
||||||
|
clientID := middleware.GetClientID(c)
|
||||||
|
user := middleware.GetUser(c)
|
||||||
|
uid := 0
|
||||||
|
if user != nil {
|
||||||
|
uid = int(user.ID)
|
||||||
|
}
|
||||||
|
publicURL, err := _i.svc.Upload(clientID, uid, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"File uploaded and added to media library"},
|
||||||
|
Data: map[string]string{"public_url": publicURL},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *MediaLibraryController) Delete(c *fiber.Ctx) error {
|
||||||
|
clientID := middleware.GetClientID(c)
|
||||||
|
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := _i.svc.Delete(clientID, uint(id)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
|
Messages: utilRes.Messages{"Media library entry removed"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
package media_library
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"go.uber.org/fx"
|
||||||
|
|
||||||
|
"web-qudo-be/app/module/media_library/controller"
|
||||||
|
"web-qudo-be/app/module/media_library/repository"
|
||||||
|
"web-qudo-be/app/module/media_library/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MediaLibraryRouter struct {
|
||||||
|
App *fiber.App
|
||||||
|
Ctrl *controller.MediaLibraryController
|
||||||
|
}
|
||||||
|
|
||||||
|
var NewMediaLibraryModule = fx.Options(
|
||||||
|
fx.Provide(repository.NewMediaLibraryRepository),
|
||||||
|
fx.Provide(service.NewMediaLibraryService),
|
||||||
|
fx.Provide(controller.NewMediaLibraryController),
|
||||||
|
fx.Provide(NewMediaLibraryRouter),
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewMediaLibraryRouter(app *fiber.App, ctrl *controller.MediaLibraryController) *MediaLibraryRouter {
|
||||||
|
return &MediaLibraryRouter{App: app, Ctrl: ctrl}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MediaLibraryRouter) RegisterMediaLibraryRoutes() {
|
||||||
|
c := r.Ctrl
|
||||||
|
r.App.Route("/media-library", func(router fiber.Router) {
|
||||||
|
router.Get("/", c.All)
|
||||||
|
router.Post("/register", c.Register)
|
||||||
|
router.Post("/upload", c.Upload)
|
||||||
|
router.Delete("/:id", c.Delete)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"web-qudo-be/app/database"
|
||||||
|
"web-qudo-be/app/database/entity"
|
||||||
|
"web-qudo-be/utils/paginator"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MediaLibraryRepository interface {
|
||||||
|
FindByPublicURLAny(publicURL string) (*entity.MediaLibraryItem, error)
|
||||||
|
Create(item *entity.MediaLibraryItem) error
|
||||||
|
Update(id uint, fields map[string]interface{}) error
|
||||||
|
GetAll(clientID *uuid.UUID, q string, sourceType *string, p *paginator.Pagination) ([]*entity.MediaLibraryItem, *paginator.Pagination, error)
|
||||||
|
SoftDelete(clientID *uuid.UUID, id uint) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type mediaLibraryRepository struct {
|
||||||
|
DB *database.Database
|
||||||
|
Log zerolog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMediaLibraryRepository(db *database.Database, log zerolog.Logger) MediaLibraryRepository {
|
||||||
|
return &mediaLibraryRepository{DB: db, Log: log}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *mediaLibraryRepository) FindByPublicURLAny(publicURL string) (*entity.MediaLibraryItem, error) {
|
||||||
|
var row entity.MediaLibraryItem
|
||||||
|
err := _i.DB.DB.Where("public_url = ?", publicURL).First(&row).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &row, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *mediaLibraryRepository) Create(item *entity.MediaLibraryItem) error {
|
||||||
|
return _i.DB.DB.Create(item).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *mediaLibraryRepository) Update(id uint, fields map[string]interface{}) error {
|
||||||
|
return _i.DB.DB.Model(&entity.MediaLibraryItem{}).Where("id = ?", id).Updates(fields).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *mediaLibraryRepository) GetAll(clientID *uuid.UUID, q string, sourceType *string, p *paginator.Pagination) ([]*entity.MediaLibraryItem, *paginator.Pagination, error) {
|
||||||
|
var rows []*entity.MediaLibraryItem
|
||||||
|
var count int64
|
||||||
|
|
||||||
|
query := _i.DB.DB.Model(&entity.MediaLibraryItem{}).Where("is_active = ?", true)
|
||||||
|
if clientID != nil {
|
||||||
|
query = query.Where("client_id = ?", clientID)
|
||||||
|
}
|
||||||
|
if sourceType != nil && strings.TrimSpace(*sourceType) != "" {
|
||||||
|
query = query.Where("source_type = ?", strings.TrimSpace(*sourceType))
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(q) != "" {
|
||||||
|
like := "%" + strings.ToLower(strings.TrimSpace(q)) + "%"
|
||||||
|
query = query.Where(
|
||||||
|
"LOWER(COALESCE(original_filename,'')) LIKE ? OR LOWER(public_url) LIKE ? OR LOWER(COALESCE(source_label,'')) LIKE ?",
|
||||||
|
like, like, like,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if err := query.Count(&count).Error; err != nil {
|
||||||
|
return nil, p, err
|
||||||
|
}
|
||||||
|
p.Count = count
|
||||||
|
p = paginator.Paging(p)
|
||||||
|
err := query.Order("created_at DESC").Offset(p.Offset).Limit(p.Limit).Find(&rows).Error
|
||||||
|
return rows, p, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_i *mediaLibraryRepository) SoftDelete(clientID *uuid.UUID, id uint) error {
|
||||||
|
q := _i.DB.DB.Model(&entity.MediaLibraryItem{}).Where("id = ?", id)
|
||||||
|
if clientID != nil {
|
||||||
|
q = q.Where("client_id = ?", clientID)
|
||||||
|
}
|
||||||
|
res := q.Update("is_active", false)
|
||||||
|
if res.Error != nil {
|
||||||
|
return res.Error
|
||||||
|
}
|
||||||
|
if res.RowsAffected == 0 {
|
||||||
|
return gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
package request
|
||||||
|
|
||||||
|
import (
|
||||||
|
"web-qudo-be/utils/paginator"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MediaLibraryRegisterRequest struct {
|
||||||
|
PublicURL string `json:"public_url" validate:"required"`
|
||||||
|
ObjectKey *string `json:"object_key"`
|
||||||
|
OriginalFilename *string `json:"original_filename"`
|
||||||
|
FileCategory *string `json:"file_category"`
|
||||||
|
SizeBytes *int64 `json:"size_bytes"`
|
||||||
|
SourceType string `json:"source_type" validate:"required"`
|
||||||
|
SourceLabel *string `json:"source_label"`
|
||||||
|
ArticleFileID *uint `json:"article_file_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MediaLibraryQueryRequest struct {
|
||||||
|
Q string `json:"q"`
|
||||||
|
SourceType *string `json:"source_type"`
|
||||||
|
Pagination *paginator.Pagination `json:"pagination"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
package response
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MediaLibraryItemResponse struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
PublicURL string `json:"public_url"`
|
||||||
|
ObjectKey *string `json:"object_key"`
|
||||||
|
OriginalFilename *string `json:"original_filename"`
|
||||||
|
FileCategory string `json:"file_category"`
|
||||||
|
SizeBytes *int64 `json:"size_bytes"`
|
||||||
|
SourceType string `json:"source_type"`
|
||||||
|
SourceLabel *string `json:"source_label"`
|
||||||
|
ArticleFileID *uint `json:"article_file_id"`
|
||||||
|
CreatedByID int `json:"created_by_id"`
|
||||||
|
ClientID *uuid.UUID `json:"client_id"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,245 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"mime/multipart"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"web-qudo-be/app/database/entity"
|
||||||
|
"web-qudo-be/app/module/media_library/repository"
|
||||||
|
"web-qudo-be/app/module/media_library/request"
|
||||||
|
"web-qudo-be/app/module/media_library/response"
|
||||||
|
appcfg "web-qudo-be/config/config"
|
||||||
|
"web-qudo-be/utils/paginator"
|
||||||
|
"web-qudo-be/utils/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RegisterInput is used from article/CMS upload hooks (same physical file, extra catalog row).
|
||||||
|
type RegisterInput struct {
|
||||||
|
ClientID *uuid.UUID
|
||||||
|
UserID int
|
||||||
|
PublicURL string
|
||||||
|
ObjectKey *string
|
||||||
|
OriginalFilename *string
|
||||||
|
FileCategory string
|
||||||
|
SizeBytes *int64
|
||||||
|
SourceType string
|
||||||
|
SourceLabel *string
|
||||||
|
ArticleFileID *uint
|
||||||
|
}
|
||||||
|
|
||||||
|
type MediaLibraryService interface {
|
||||||
|
UpsertRegister(in RegisterInput) error
|
||||||
|
RegisterFromRequest(clientID *uuid.UUID, userID int, req *request.MediaLibraryRegisterRequest) error
|
||||||
|
RegisterCMSAsset(publicURL, objectKey, sourceLabel string, file *multipart.FileHeader) error
|
||||||
|
All(clientID *uuid.UUID, q string, sourceType *string, p *paginator.Pagination) ([]*response.MediaLibraryItemResponse, *paginator.Pagination, error)
|
||||||
|
Upload(clientID *uuid.UUID, userID int, c *fiber.Ctx) (publicURL string, err error)
|
||||||
|
Delete(clientID *uuid.UUID, id uint) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type mediaLibraryService struct {
|
||||||
|
Repo repository.MediaLibraryRepository
|
||||||
|
Cfg *appcfg.Config
|
||||||
|
MinioStorage *appcfg.MinioStorage
|
||||||
|
Log zerolog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMediaLibraryService(
|
||||||
|
repo repository.MediaLibraryRepository,
|
||||||
|
cfg *appcfg.Config,
|
||||||
|
minio *appcfg.MinioStorage,
|
||||||
|
log zerolog.Logger,
|
||||||
|
) MediaLibraryService {
|
||||||
|
return &mediaLibraryService{Repo: repo, Cfg: cfg, MinioStorage: minio, Log: log}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ArticleFilePublicURL(cfg *appcfg.Config, fileName string) string {
|
||||||
|
base := strings.TrimSuffix(cfg.App.Domain, "/")
|
||||||
|
return base + "/article-files/viewer/" + strings.TrimPrefix(fileName, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func CategoryFromFilename(name string) string {
|
||||||
|
ext := strings.ToLower(filepath.Ext(name))
|
||||||
|
switch ext {
|
||||||
|
case ".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg":
|
||||||
|
return "image"
|
||||||
|
case ".mp4", ".webm", ".mov":
|
||||||
|
return "video"
|
||||||
|
case ".mp3", ".wav", ".ogg", ".m4a":
|
||||||
|
return "audio"
|
||||||
|
case ".pdf", ".doc", ".docx", ".txt", ".csv":
|
||||||
|
return "document"
|
||||||
|
default:
|
||||||
|
return "other"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *mediaLibraryService) UpsertRegister(in RegisterInput) error {
|
||||||
|
url := strings.TrimSpace(in.PublicURL)
|
||||||
|
if url == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
existing, err := s.Repo.FindByPublicURLAny(url)
|
||||||
|
if err == nil {
|
||||||
|
if !existing.IsActive {
|
||||||
|
cat := strings.TrimSpace(in.FileCategory)
|
||||||
|
if cat == "" {
|
||||||
|
cat = "other"
|
||||||
|
}
|
||||||
|
return s.Repo.Update(existing.ID, map[string]interface{}{
|
||||||
|
"is_active": true,
|
||||||
|
"object_key": in.ObjectKey,
|
||||||
|
"original_filename": in.OriginalFilename,
|
||||||
|
"file_category": cat,
|
||||||
|
"size_bytes": in.SizeBytes,
|
||||||
|
"source_type": in.SourceType,
|
||||||
|
"source_label": in.SourceLabel,
|
||||||
|
"article_file_id": in.ArticleFileID,
|
||||||
|
"updated_at": time.Now(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cat := strings.TrimSpace(in.FileCategory)
|
||||||
|
if cat == "" {
|
||||||
|
cat = "other"
|
||||||
|
}
|
||||||
|
item := &entity.MediaLibraryItem{
|
||||||
|
PublicURL: url,
|
||||||
|
ObjectKey: in.ObjectKey,
|
||||||
|
OriginalFilename: in.OriginalFilename,
|
||||||
|
FileCategory: cat,
|
||||||
|
SizeBytes: in.SizeBytes,
|
||||||
|
SourceType: in.SourceType,
|
||||||
|
SourceLabel: in.SourceLabel,
|
||||||
|
ArticleFileID: in.ArticleFileID,
|
||||||
|
CreatedByID: in.UserID,
|
||||||
|
ClientID: in.ClientID,
|
||||||
|
IsActive: true,
|
||||||
|
}
|
||||||
|
return s.Repo.Create(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *mediaLibraryService) RegisterCMSAsset(publicURL, objectKey, sourceLabel string, file *multipart.FileHeader) error {
|
||||||
|
if strings.TrimSpace(publicURL) == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var keyPtr *string
|
||||||
|
if strings.TrimSpace(objectKey) != "" {
|
||||||
|
k := objectKey
|
||||||
|
keyPtr = &k
|
||||||
|
}
|
||||||
|
lbl := sourceLabel
|
||||||
|
var namePtr *string
|
||||||
|
var sz *int64
|
||||||
|
cat := "other"
|
||||||
|
if file != nil {
|
||||||
|
b := filepath.Base(file.Filename)
|
||||||
|
namePtr = &b
|
||||||
|
ss := file.Size
|
||||||
|
sz = &ss
|
||||||
|
cat = CategoryFromFilename(b)
|
||||||
|
}
|
||||||
|
return s.UpsertRegister(RegisterInput{
|
||||||
|
PublicURL: publicURL,
|
||||||
|
ObjectKey: keyPtr,
|
||||||
|
OriginalFilename: namePtr,
|
||||||
|
FileCategory: cat,
|
||||||
|
SizeBytes: sz,
|
||||||
|
SourceType: "cms",
|
||||||
|
SourceLabel: &lbl,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *mediaLibraryService) RegisterFromRequest(clientID *uuid.UUID, userID int, req *request.MediaLibraryRegisterRequest) error {
|
||||||
|
cat := CategoryFromFilename(req.PublicURL)
|
||||||
|
if req.OriginalFilename != nil {
|
||||||
|
cat = CategoryFromFilename(*req.OriginalFilename)
|
||||||
|
}
|
||||||
|
if req.FileCategory != nil && strings.TrimSpace(*req.FileCategory) != "" {
|
||||||
|
cat = strings.TrimSpace(*req.FileCategory)
|
||||||
|
}
|
||||||
|
return s.UpsertRegister(RegisterInput{
|
||||||
|
ClientID: clientID,
|
||||||
|
UserID: userID,
|
||||||
|
PublicURL: strings.TrimSpace(req.PublicURL),
|
||||||
|
ObjectKey: req.ObjectKey,
|
||||||
|
OriginalFilename: req.OriginalFilename,
|
||||||
|
FileCategory: cat,
|
||||||
|
SizeBytes: req.SizeBytes,
|
||||||
|
SourceType: req.SourceType,
|
||||||
|
SourceLabel: req.SourceLabel,
|
||||||
|
ArticleFileID: req.ArticleFileID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func toResponse(e *entity.MediaLibraryItem) *response.MediaLibraryItemResponse {
|
||||||
|
return &response.MediaLibraryItemResponse{
|
||||||
|
ID: e.ID,
|
||||||
|
PublicURL: e.PublicURL,
|
||||||
|
ObjectKey: e.ObjectKey,
|
||||||
|
OriginalFilename: e.OriginalFilename,
|
||||||
|
FileCategory: e.FileCategory,
|
||||||
|
SizeBytes: e.SizeBytes,
|
||||||
|
SourceType: e.SourceType,
|
||||||
|
SourceLabel: e.SourceLabel,
|
||||||
|
ArticleFileID: e.ArticleFileID,
|
||||||
|
CreatedByID: e.CreatedByID,
|
||||||
|
ClientID: e.ClientID,
|
||||||
|
CreatedAt: e.CreatedAt,
|
||||||
|
UpdatedAt: e.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *mediaLibraryService) All(clientID *uuid.UUID, q string, sourceType *string, p *paginator.Pagination) ([]*response.MediaLibraryItemResponse, *paginator.Pagination, error) {
|
||||||
|
rows, paging, err := s.Repo.GetAll(clientID, q, sourceType, p)
|
||||||
|
if err != nil {
|
||||||
|
return nil, paging, err
|
||||||
|
}
|
||||||
|
out := make([]*response.MediaLibraryItemResponse, 0, len(rows))
|
||||||
|
for _, r := range rows {
|
||||||
|
out = append(out, toResponse(r))
|
||||||
|
}
|
||||||
|
return out, paging, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *mediaLibraryService) Upload(clientID *uuid.UUID, userID int, c *fiber.Ctx) (string, error) {
|
||||||
|
file, err := c.FormFile("file")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
key, previewURL, err := storage.UploadMediaLibraryObject(s.MinioStorage, file)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
name := filepath.Base(file.Filename)
|
||||||
|
sz := file.Size
|
||||||
|
err = s.UpsertRegister(RegisterInput{
|
||||||
|
ClientID: clientID,
|
||||||
|
UserID: userID,
|
||||||
|
PublicURL: previewURL,
|
||||||
|
ObjectKey: &key,
|
||||||
|
OriginalFilename: &name,
|
||||||
|
FileCategory: CategoryFromFilename(name),
|
||||||
|
SizeBytes: &sz,
|
||||||
|
SourceType: "upload",
|
||||||
|
SourceLabel: strPtr("media_library_direct"),
|
||||||
|
})
|
||||||
|
return previewURL, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func strPtr(s string) *string { return &s }
|
||||||
|
|
||||||
|
func (s *mediaLibraryService) Delete(clientID *uuid.UUID, id uint) error {
|
||||||
|
return s.Repo.SoftDelete(clientID, id)
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
|
@ -32,7 +34,7 @@ func NewOurProductContentImagesController(service service.OurProductContentImage
|
||||||
}
|
}
|
||||||
|
|
||||||
func (_i *ourProductContentImagesController) FindByOurProductContentID(c *fiber.Ctx) error {
|
func (_i *ourProductContentImagesController) FindByOurProductContentID(c *fiber.Ctx) error {
|
||||||
contentIDStr := c.Params("our_product_content_id")
|
contentIDStr := c.Params("content_id")
|
||||||
|
|
||||||
contentID, err := uuid.Parse(contentIDStr)
|
contentID, err := uuid.Parse(contentIDStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -53,6 +55,28 @@ func (_i *ourProductContentImagesController) FindByOurProductContentID(c *fiber.
|
||||||
}
|
}
|
||||||
|
|
||||||
func (_i *ourProductContentImagesController) Save(c *fiber.Ctx) error {
|
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)
|
req := new(request.OurProductContentImageCreateRequest)
|
||||||
|
|
||||||
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
||||||
|
|
@ -82,6 +106,21 @@ func (_i *ourProductContentImagesController) Update(c *fiber.Ctx) error {
|
||||||
return err
|
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)
|
req := new(request.OurProductContentImageUpdateRequest)
|
||||||
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
|
|
@ -55,10 +55,18 @@ func (r *ourProductContentImagesRepository) Create(data *entity.OurProductConten
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ourProductContentImagesRepository) Update(id uuid.UUID, data *entity.OurProductContentImage) error {
|
func (r *ourProductContentImagesRepository) Update(id uuid.UUID, data *entity.OurProductContentImage) error {
|
||||||
|
updates := map[string]interface{}{
|
||||||
|
"image_path": data.ImagePath,
|
||||||
|
"image_url": data.ImageURL,
|
||||||
|
}
|
||||||
|
if data.IsThumbnail != nil {
|
||||||
|
updates["is_thumbnail"] = data.IsThumbnail
|
||||||
|
}
|
||||||
|
|
||||||
err := r.DB.DB.
|
err := r.DB.DB.
|
||||||
Model(&entity.OurProductContentImage{}).
|
Model(&entity.OurProductContentImage{}).
|
||||||
Where("id = ?", id).
|
Where("id = ?", id).
|
||||||
Updates(data).Error
|
Updates(updates).Error
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.Log.Error().Err(err).Msg("failed update our product content image")
|
r.Log.Error().Err(err).Msg("failed update our product content image")
|
||||||
|
|
@ -76,4 +84,4 @@ func (r *ourProductContentImagesRepository) Delete(id uuid.UUID) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,45 @@
|
||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"mime/multipart"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
|
||||||
"web-qudo-be/app/database/entity"
|
"web-qudo-be/app/database/entity"
|
||||||
|
medialib "web-qudo-be/app/module/media_library/service"
|
||||||
"web-qudo-be/app/module/our_product_content_images/repository"
|
"web-qudo-be/app/module/our_product_content_images/repository"
|
||||||
|
minioStorage "web-qudo-be/config/config"
|
||||||
|
"web-qudo-be/utils/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ourProductContentImagesService struct {
|
type ourProductContentImagesService struct {
|
||||||
Repo repository.OurProductContentImagesRepository
|
Repo repository.OurProductContentImagesRepository
|
||||||
Log zerolog.Logger
|
MinioStorage *minioStorage.MinioStorage
|
||||||
|
MediaLib medialib.MediaLibraryService
|
||||||
|
Log zerolog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
type OurProductContentImagesService interface {
|
type OurProductContentImagesService interface {
|
||||||
FindByContentID(contentID uuid.UUID) ([]entity.OurProductContentImage, error)
|
FindByContentID(contentID uuid.UUID) ([]entity.OurProductContentImage, error)
|
||||||
Save(data *entity.OurProductContentImage) (*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
|
Update(id uuid.UUID, data *entity.OurProductContentImage) error
|
||||||
|
UpdateWithFile(id uuid.UUID, file *multipart.FileHeader) error
|
||||||
Delete(id uuid.UUID) error
|
Delete(id uuid.UUID) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewOurProductContentImagesService(
|
func NewOurProductContentImagesService(
|
||||||
repo repository.OurProductContentImagesRepository,
|
repo repository.OurProductContentImagesRepository,
|
||||||
|
minio *minioStorage.MinioStorage,
|
||||||
|
mediaLib medialib.MediaLibraryService,
|
||||||
log zerolog.Logger,
|
log zerolog.Logger,
|
||||||
) OurProductContentImagesService {
|
) OurProductContentImagesService {
|
||||||
return &ourProductContentImagesService{
|
return &ourProductContentImagesService{
|
||||||
Repo: repo,
|
Repo: repo,
|
||||||
Log: log,
|
MinioStorage: minio,
|
||||||
|
MediaLib: mediaLib,
|
||||||
|
Log: log,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,6 +65,26 @@ func (s *ourProductContentImagesService) Save(data *entity.OurProductContentImag
|
||||||
return result, nil
|
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,
|
||||||
|
}
|
||||||
|
out, err := s.Save(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if s.MediaLib != nil {
|
||||||
|
_ = s.MediaLib.RegisterCMSAsset(url, key, "our_product", file)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *ourProductContentImagesService) Update(id uuid.UUID, data *entity.OurProductContentImage) error {
|
func (s *ourProductContentImagesService) Update(id uuid.UUID, data *entity.OurProductContentImage) error {
|
||||||
err := s.Repo.Update(id, data)
|
err := s.Repo.Update(id, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -62,6 +95,23 @@ func (s *ourProductContentImagesService) Update(id uuid.UUID, data *entity.OurPr
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
if err := s.Repo.Update(id, &entity.OurProductContentImage{
|
||||||
|
ImagePath: key,
|
||||||
|
ImageURL: url,
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if s.MediaLib != nil {
|
||||||
|
_ = s.MediaLib.RegisterCMSAsset(url, key, "our_product", file)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *ourProductContentImagesService) Delete(id uuid.UUID) error {
|
func (s *ourProductContentImagesService) Delete(id uuid.UUID) error {
|
||||||
err := s.Repo.Delete(id)
|
err := s.Repo.Delete(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -70,4 +120,4 @@ func (s *ourProductContentImagesService) Delete(id uuid.UUID) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
package repository
|
package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
"web-qudo-be/app/database"
|
"web-qudo-be/app/database"
|
||||||
"web-qudo-be/app/database/entity"
|
"web-qudo-be/app/database/entity"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ourProductContentRepository struct {
|
type ourProductContentRepository struct {
|
||||||
|
|
@ -15,6 +18,7 @@ type ourProductContentRepository struct {
|
||||||
|
|
||||||
type OurProductContentRepository interface {
|
type OurProductContentRepository interface {
|
||||||
Get() (*entity.OurProductContent, error)
|
Get() (*entity.OurProductContent, error)
|
||||||
|
GetAll() ([]entity.OurProductContent, error)
|
||||||
Create(data *entity.OurProductContent) (*entity.OurProductContent, error)
|
Create(data *entity.OurProductContent) (*entity.OurProductContent, error)
|
||||||
Update(id uuid.UUID, data *entity.OurProductContent) error
|
Update(id uuid.UUID, data *entity.OurProductContent) error
|
||||||
Delete(id uuid.UUID) error
|
Delete(id uuid.UUID) error
|
||||||
|
|
@ -35,6 +39,9 @@ func (r *ourProductContentRepository) Get() (*entity.OurProductContent, error) {
|
||||||
First(&data).Error
|
First(&data).Error
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
r.Log.Error().Err(err).Msg("failed get our product content")
|
r.Log.Error().Err(err).Msg("failed get our product content")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -42,6 +49,19 @@ func (r *ourProductContentRepository) Get() (*entity.OurProductContent, error) {
|
||||||
return &data, nil
|
return &data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *ourProductContentRepository) GetAll() ([]entity.OurProductContent, error) {
|
||||||
|
var rows []entity.OurProductContent
|
||||||
|
err := r.DB.DB.
|
||||||
|
Preload("Images").
|
||||||
|
Order("created_at ASC").
|
||||||
|
Find(&rows).Error
|
||||||
|
if err != nil {
|
||||||
|
r.Log.Error().Err(err).Msg("failed list our product contents")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *ourProductContentRepository) Create(data *entity.OurProductContent) (*entity.OurProductContent, error) {
|
func (r *ourProductContentRepository) Create(data *entity.OurProductContent) (*entity.OurProductContent, error) {
|
||||||
data.ID = uuid.New()
|
data.ID = uuid.New()
|
||||||
|
|
||||||
|
|
@ -55,10 +75,21 @@ func (r *ourProductContentRepository) Create(data *entity.OurProductContent) (*e
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ourProductContentRepository) Update(id uuid.UUID, data *entity.OurProductContent) error {
|
func (r *ourProductContentRepository) Update(id uuid.UUID, data *entity.OurProductContent) error {
|
||||||
|
updates := map[string]interface{}{
|
||||||
|
"primary_title": data.PrimaryTitle,
|
||||||
|
"secondary_title": data.SecondaryTitle,
|
||||||
|
"description": data.Description,
|
||||||
|
"link_url": data.LinkURL,
|
||||||
|
"updated_at": time.Now(),
|
||||||
|
}
|
||||||
|
if data.IsActive != nil {
|
||||||
|
updates["is_active"] = data.IsActive
|
||||||
|
}
|
||||||
|
|
||||||
err := r.DB.DB.
|
err := r.DB.DB.
|
||||||
Model(&entity.OurProductContent{}).
|
Model(&entity.OurProductContent{}).
|
||||||
Where("id = ?", id).
|
Where("id = ?", id).
|
||||||
Updates(data).Error
|
Updates(updates).Error
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.Log.Error().Err(err).Msg("failed update our product content")
|
r.Log.Error().Err(err).Msg("failed update our product content")
|
||||||
|
|
@ -68,12 +99,17 @@ func (r *ourProductContentRepository) Update(id uuid.UUID, data *entity.OurProdu
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete removes child images first so FK constraints do not block the parent delete.
|
||||||
func (r *ourProductContentRepository) Delete(id uuid.UUID) error {
|
func (r *ourProductContentRepository) Delete(id uuid.UUID) error {
|
||||||
err := r.DB.DB.Delete(&entity.OurProductContent{}, id).Error
|
return r.DB.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
if err != nil {
|
if err := tx.Exec(`DELETE FROM our_product_content_images WHERE our_product_content_id = ?`, id).Error; err != nil {
|
||||||
r.Log.Error().Err(err).Msg("failed delete our product content")
|
r.Log.Error().Err(err).Msg("failed delete our product content images")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := tx.Exec(`DELETE FROM our_product_contents WHERE id = ?`, id).Error; err != nil {
|
||||||
return nil
|
r.Log.Error().Err(err).Msg("failed delete our product content")
|
||||||
}
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ type OurProductContentCreateRequest struct {
|
||||||
PrimaryTitle string `json:"primary_title" form:"primary_title" validate:"required"`
|
PrimaryTitle string `json:"primary_title" form:"primary_title" validate:"required"`
|
||||||
SecondaryTitle string `json:"secondary_title" form:"secondary_title"`
|
SecondaryTitle string `json:"secondary_title" form:"secondary_title"`
|
||||||
Description string `json:"description" form:"description"`
|
Description string `json:"description" form:"description"`
|
||||||
|
LinkURL string `json:"link_url" form:"link_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *OurProductContentCreateRequest) ToEntity() *entity.OurProductContent {
|
func (r *OurProductContentCreateRequest) ToEntity() *entity.OurProductContent {
|
||||||
|
|
@ -13,6 +14,7 @@ func (r *OurProductContentCreateRequest) ToEntity() *entity.OurProductContent {
|
||||||
PrimaryTitle: r.PrimaryTitle,
|
PrimaryTitle: r.PrimaryTitle,
|
||||||
SecondaryTitle: r.SecondaryTitle,
|
SecondaryTitle: r.SecondaryTitle,
|
||||||
Description: r.Description,
|
Description: r.Description,
|
||||||
|
LinkURL: r.LinkURL,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -20,6 +22,7 @@ type OurProductContentUpdateRequest struct {
|
||||||
PrimaryTitle string `json:"primary_title"`
|
PrimaryTitle string `json:"primary_title"`
|
||||||
SecondaryTitle string `json:"secondary_title"`
|
SecondaryTitle string `json:"secondary_title"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
|
LinkURL string `json:"link_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *OurProductContentUpdateRequest) ToEntity() *entity.OurProductContent {
|
func (r *OurProductContentUpdateRequest) ToEntity() *entity.OurProductContent {
|
||||||
|
|
@ -27,5 +30,6 @@ func (r *OurProductContentUpdateRequest) ToEntity() *entity.OurProductContent {
|
||||||
PrimaryTitle: r.PrimaryTitle,
|
PrimaryTitle: r.PrimaryTitle,
|
||||||
SecondaryTitle: r.SecondaryTitle,
|
SecondaryTitle: r.SecondaryTitle,
|
||||||
Description: r.Description,
|
Description: r.Description,
|
||||||
|
LinkURL: r.LinkURL,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ type ourProductContentService struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type OurProductContentService interface {
|
type OurProductContentService interface {
|
||||||
Show() (*entity.OurProductContent, error)
|
Show() ([]entity.OurProductContent, error)
|
||||||
Save(data *entity.OurProductContent) (*entity.OurProductContent, error)
|
Save(data *entity.OurProductContent) (*entity.OurProductContent, error)
|
||||||
Update(id uuid.UUID, data *entity.OurProductContent) error
|
Update(id uuid.UUID, data *entity.OurProductContent) error
|
||||||
Delete(id uuid.UUID) error
|
Delete(id uuid.UUID) error
|
||||||
|
|
@ -34,14 +34,13 @@ func NewOurProductContentService(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ourProductContentService) Show() (*entity.OurProductContent, error) {
|
func (s *ourProductContentService) Show() ([]entity.OurProductContent, error) {
|
||||||
data, err := s.Repo.Get()
|
rows, err := s.Repo.GetAll()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Error().Err(err).Msg("failed get our product content")
|
s.Log.Error().Err(err).Msg("failed list our product contents")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
return rows, nil
|
||||||
return data, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ourProductContentService) Save(data *entity.OurProductContent) (*entity.OurProductContent, error) {
|
func (s *ourProductContentService) Save(data *entity.OurProductContent) (*entity.OurProductContent, error) {
|
||||||
|
|
@ -67,13 +66,5 @@ func (s *ourProductContentService) Update(id uuid.UUID, data *entity.OurProductC
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ourProductContentService) Delete(id uuid.UUID) error {
|
func (s *ourProductContentService) Delete(id uuid.UUID) error {
|
||||||
result, err := s.Repo.Get()
|
return s.Repo.Delete(id)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
isActive := false
|
|
||||||
result.IsActive = &isActive
|
|
||||||
|
|
||||||
return s.Repo.Update(id, result)
|
|
||||||
}
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
|
@ -56,6 +57,29 @@ func (_i *ourServiceContentImagesController) FindByOurServiceContentID(c *fiber.
|
||||||
}
|
}
|
||||||
|
|
||||||
func (_i *ourServiceContentImagesController) Save(c *fiber.Ctx) error {
|
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)
|
req := new(request.OurServiceContentImageCreateRequest)
|
||||||
|
|
||||||
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
||||||
|
|
@ -87,6 +111,21 @@ func (_i *ourServiceContentImagesController) Update(c *fiber.Ctx) error {
|
||||||
|
|
||||||
id := uint(idInt)
|
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)
|
req := new(request.OurServiceContentImageUpdateRequest)
|
||||||
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
|
|
@ -52,10 +52,18 @@ func (r *ourServiceContentImagesRepository) Create(data *entity.OurServiceConten
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ourServiceContentImagesRepository) Update(id uint, data *entity.OurServiceContentImage) error {
|
func (r *ourServiceContentImagesRepository) Update(id uint, data *entity.OurServiceContentImage) error {
|
||||||
|
updates := map[string]interface{}{
|
||||||
|
"image_path": data.ImagePath,
|
||||||
|
"image_url": data.ImageURL,
|
||||||
|
}
|
||||||
|
if data.IsThumbnail != nil {
|
||||||
|
updates["is_thumbnail"] = data.IsThumbnail
|
||||||
|
}
|
||||||
|
|
||||||
err := r.DB.DB.
|
err := r.DB.DB.
|
||||||
Model(&entity.OurServiceContentImage{}).
|
Model(&entity.OurServiceContentImage{}).
|
||||||
Where("id = ?", id).
|
Where("id = ?", id).
|
||||||
Updates(data).Error
|
Updates(updates).Error
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.Log.Error().Err(err).Msg("failed update our service content images")
|
r.Log.Error().Err(err).Msg("failed update our service content images")
|
||||||
|
|
@ -73,4 +81,4 @@ func (r *ourServiceContentImagesRepository) Delete(id uint) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,44 @@
|
||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"mime/multipart"
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
|
||||||
"web-qudo-be/app/database/entity"
|
"web-qudo-be/app/database/entity"
|
||||||
|
medialib "web-qudo-be/app/module/media_library/service"
|
||||||
"web-qudo-be/app/module/our_service_content_images/repository"
|
"web-qudo-be/app/module/our_service_content_images/repository"
|
||||||
|
minioStorage "web-qudo-be/config/config"
|
||||||
|
"web-qudo-be/utils/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ourServiceContentImagesService struct {
|
type ourServiceContentImagesService struct {
|
||||||
Repo repository.OurServiceContentImagesRepository
|
Repo repository.OurServiceContentImagesRepository
|
||||||
Log zerolog.Logger
|
MinioStorage *minioStorage.MinioStorage
|
||||||
|
MediaLib medialib.MediaLibraryService
|
||||||
|
Log zerolog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
type OurServiceContentImagesService interface {
|
type OurServiceContentImagesService interface {
|
||||||
FindByContentID(contentID uint) ([]entity.OurServiceContentImage, error)
|
FindByContentID(contentID uint) ([]entity.OurServiceContentImage, error)
|
||||||
Save(data *entity.OurServiceContentImage) (*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
|
Update(id uint, data *entity.OurServiceContentImage) error
|
||||||
|
UpdateWithFile(id uint, file *multipart.FileHeader) error
|
||||||
Delete(id uint) error
|
Delete(id uint) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewOurServiceContentImagesService(
|
func NewOurServiceContentImagesService(
|
||||||
repo repository.OurServiceContentImagesRepository,
|
repo repository.OurServiceContentImagesRepository,
|
||||||
|
minio *minioStorage.MinioStorage,
|
||||||
|
mediaLib medialib.MediaLibraryService,
|
||||||
log zerolog.Logger,
|
log zerolog.Logger,
|
||||||
) OurServiceContentImagesService {
|
) OurServiceContentImagesService {
|
||||||
return &ourServiceContentImagesService{
|
return &ourServiceContentImagesService{
|
||||||
Repo: repo,
|
Repo: repo,
|
||||||
Log: log,
|
MinioStorage: minio,
|
||||||
|
MediaLib: mediaLib,
|
||||||
|
Log: log,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -49,6 +62,26 @@ func (s *ourServiceContentImagesService) Save(data *entity.OurServiceContentImag
|
||||||
return result, nil
|
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,
|
||||||
|
}
|
||||||
|
out, err := s.Save(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if s.MediaLib != nil {
|
||||||
|
_ = s.MediaLib.RegisterCMSAsset(url, key, "our_service", file)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *ourServiceContentImagesService) Update(id uint, data *entity.OurServiceContentImage) error {
|
func (s *ourServiceContentImagesService) Update(id uint, data *entity.OurServiceContentImage) error {
|
||||||
err := s.Repo.Update(id, data)
|
err := s.Repo.Update(id, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -59,6 +92,23 @@ func (s *ourServiceContentImagesService) Update(id uint, data *entity.OurService
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
if err := s.Repo.Update(id, &entity.OurServiceContentImage{
|
||||||
|
ImagePath: key,
|
||||||
|
ImageURL: url,
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if s.MediaLib != nil {
|
||||||
|
_ = s.MediaLib.RegisterCMSAsset(url, key, "our_service", file)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *ourServiceContentImagesService) Delete(id uint) error {
|
func (s *ourServiceContentImagesService) Delete(id uint) error {
|
||||||
err := s.Repo.Delete(id)
|
err := s.Repo.Delete(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -67,4 +117,4 @@ func (s *ourServiceContentImagesService) Delete(id uint) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
package repository
|
package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
"web-qudo-be/app/database"
|
"web-qudo-be/app/database"
|
||||||
"web-qudo-be/app/database/entity"
|
"web-qudo-be/app/database/entity"
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ourServiceContentRepository struct {
|
type ourServiceContentRepository struct {
|
||||||
|
|
@ -14,6 +17,7 @@ type ourServiceContentRepository struct {
|
||||||
|
|
||||||
type OurServiceContentRepository interface {
|
type OurServiceContentRepository interface {
|
||||||
Get() (*entity.OurServiceContent, error)
|
Get() (*entity.OurServiceContent, error)
|
||||||
|
GetAll() ([]entity.OurServiceContent, error)
|
||||||
Create(data *entity.OurServiceContent) (*entity.OurServiceContent, error)
|
Create(data *entity.OurServiceContent) (*entity.OurServiceContent, error)
|
||||||
Update(id uint, data *entity.OurServiceContent) error
|
Update(id uint, data *entity.OurServiceContent) error
|
||||||
Delete(id uint) error
|
Delete(id uint) error
|
||||||
|
|
@ -34,6 +38,9 @@ func (r *ourServiceContentRepository) Get() (*entity.OurServiceContent, error) {
|
||||||
First(&data).Error
|
First(&data).Error
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
r.Log.Error().Err(err).Msg("failed get our service content")
|
r.Log.Error().Err(err).Msg("failed get our service content")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -41,6 +48,19 @@ func (r *ourServiceContentRepository) Get() (*entity.OurServiceContent, error) {
|
||||||
return &data, nil
|
return &data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *ourServiceContentRepository) GetAll() ([]entity.OurServiceContent, error) {
|
||||||
|
var rows []entity.OurServiceContent
|
||||||
|
err := r.DB.DB.
|
||||||
|
Preload("Images").
|
||||||
|
Order("created_at ASC").
|
||||||
|
Find(&rows).Error
|
||||||
|
if err != nil {
|
||||||
|
r.Log.Error().Err(err).Msg("failed list our service contents")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *ourServiceContentRepository) Create(data *entity.OurServiceContent) (*entity.OurServiceContent, error) {
|
func (r *ourServiceContentRepository) Create(data *entity.OurServiceContent) (*entity.OurServiceContent, error) {
|
||||||
err := r.DB.DB.Create(data).Error
|
err := r.DB.DB.Create(data).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -52,10 +72,21 @@ func (r *ourServiceContentRepository) Create(data *entity.OurServiceContent) (*e
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ourServiceContentRepository) Update(id uint, data *entity.OurServiceContent) error {
|
func (r *ourServiceContentRepository) Update(id uint, data *entity.OurServiceContent) error {
|
||||||
|
updates := map[string]interface{}{
|
||||||
|
"primary_title": data.PrimaryTitle,
|
||||||
|
"secondary_title": data.SecondaryTitle,
|
||||||
|
"description": data.Description,
|
||||||
|
"link_url": data.LinkURL,
|
||||||
|
"updated_at": time.Now(),
|
||||||
|
}
|
||||||
|
if data.IsActive != nil {
|
||||||
|
updates["is_active"] = data.IsActive
|
||||||
|
}
|
||||||
|
|
||||||
err := r.DB.DB.
|
err := r.DB.DB.
|
||||||
Model(&entity.OurServiceContent{}).
|
Model(&entity.OurServiceContent{}).
|
||||||
Where("id = ?", id).
|
Where("id = ?", id).
|
||||||
Updates(data).Error
|
Updates(updates).Error
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.Log.Error().Err(err).Msg("failed update our service content")
|
r.Log.Error().Err(err).Msg("failed update our service content")
|
||||||
|
|
@ -65,12 +96,17 @@ func (r *ourServiceContentRepository) Update(id uint, data *entity.OurServiceCon
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete removes child images first so FK constraints do not block the parent delete.
|
||||||
func (r *ourServiceContentRepository) Delete(id uint) error {
|
func (r *ourServiceContentRepository) Delete(id uint) error {
|
||||||
err := r.DB.DB.Delete(&entity.OurServiceContent{}, id).Error
|
return r.DB.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
if err != nil {
|
if err := tx.Exec(`DELETE FROM our_service_content_images WHERE our_service_content_id = ?`, id).Error; err != nil {
|
||||||
r.Log.Error().Err(err).Msg("failed delete our service content")
|
r.Log.Error().Err(err).Msg("failed delete our service content images")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := tx.Exec(`DELETE FROM our_service_contents WHERE id = ?`, id).Error; err != nil {
|
||||||
return nil
|
r.Log.Error().Err(err).Msg("failed delete our service content")
|
||||||
}
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ type OurServiceContentCreateRequest struct {
|
||||||
PrimaryTitle string `json:"primary_title" form:"primary_title" validate:"required"`
|
PrimaryTitle string `json:"primary_title" form:"primary_title" validate:"required"`
|
||||||
SecondaryTitle string `json:"secondary_title" form:"secondary_title"`
|
SecondaryTitle string `json:"secondary_title" form:"secondary_title"`
|
||||||
Description string `json:"description" form:"description"`
|
Description string `json:"description" form:"description"`
|
||||||
|
LinkURL string `json:"link_url" form:"link_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *OurServiceContentCreateRequest) ToEntity() *entity.OurServiceContent {
|
func (r *OurServiceContentCreateRequest) ToEntity() *entity.OurServiceContent {
|
||||||
|
|
@ -13,6 +14,7 @@ func (r *OurServiceContentCreateRequest) ToEntity() *entity.OurServiceContent {
|
||||||
PrimaryTitle: r.PrimaryTitle,
|
PrimaryTitle: r.PrimaryTitle,
|
||||||
SecondaryTitle: r.SecondaryTitle,
|
SecondaryTitle: r.SecondaryTitle,
|
||||||
Description: r.Description,
|
Description: r.Description,
|
||||||
|
LinkURL: r.LinkURL,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -20,6 +22,7 @@ type OurServiceContentUpdateRequest struct {
|
||||||
PrimaryTitle string `json:"primary_title"`
|
PrimaryTitle string `json:"primary_title"`
|
||||||
SecondaryTitle string `json:"secondary_title"`
|
SecondaryTitle string `json:"secondary_title"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
|
LinkURL string `json:"link_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *OurServiceContentUpdateRequest) ToEntity() *entity.OurServiceContent {
|
func (r *OurServiceContentUpdateRequest) ToEntity() *entity.OurServiceContent {
|
||||||
|
|
@ -27,5 +30,6 @@ func (r *OurServiceContentUpdateRequest) ToEntity() *entity.OurServiceContent {
|
||||||
PrimaryTitle: r.PrimaryTitle,
|
PrimaryTitle: r.PrimaryTitle,
|
||||||
SecondaryTitle: r.SecondaryTitle,
|
SecondaryTitle: r.SecondaryTitle,
|
||||||
Description: r.Description,
|
Description: r.Description,
|
||||||
|
LinkURL: r.LinkURL,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ type ourServiceContentService struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type OurServiceContentService interface {
|
type OurServiceContentService interface {
|
||||||
Show() (*entity.OurServiceContent, error)
|
Show() ([]entity.OurServiceContent, error)
|
||||||
Save(data *entity.OurServiceContent) (*entity.OurServiceContent, error)
|
Save(data *entity.OurServiceContent) (*entity.OurServiceContent, error)
|
||||||
Update(id uint, data *entity.OurServiceContent) error
|
Update(id uint, data *entity.OurServiceContent) error
|
||||||
Delete(id uint) error
|
Delete(id uint) error
|
||||||
|
|
@ -33,14 +33,13 @@ func NewOurServiceContentService(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ourServiceContentService) Show() (*entity.OurServiceContent, error) {
|
func (s *ourServiceContentService) Show() ([]entity.OurServiceContent, error) {
|
||||||
data, err := s.Repo.Get()
|
rows, err := s.Repo.GetAll()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Error().Err(err).Msg("failed get our service content")
|
s.Log.Error().Err(err).Msg("failed list our service contents")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
return rows, nil
|
||||||
return data, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ourServiceContentService) Save(data *entity.OurServiceContent) (*entity.OurServiceContent, error) {
|
func (s *ourServiceContentService) Save(data *entity.OurServiceContent) (*entity.OurServiceContent, error) {
|
||||||
|
|
@ -75,13 +74,5 @@ func (s *ourServiceContentService) Update(id uint, data *entity.OurServiceConten
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ourServiceContentService) Delete(id uint) error {
|
func (s *ourServiceContentService) Delete(id uint) error {
|
||||||
result, err := s.Repo.Get()
|
return s.Repo.Delete(id)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
isActive := false
|
|
||||||
result.IsActive = &isActive
|
|
||||||
|
|
||||||
return s.Repo.Update(id, result)
|
|
||||||
}
|
}
|
||||||
|
|
@ -21,6 +21,7 @@ type PartnerContentController interface {
|
||||||
Show(c *fiber.Ctx) error
|
Show(c *fiber.Ctx) error
|
||||||
Save(c *fiber.Ctx) error
|
Save(c *fiber.Ctx) error
|
||||||
Update(c *fiber.Ctx) error
|
Update(c *fiber.Ctx) error
|
||||||
|
UploadLogo(c *fiber.Ctx) error
|
||||||
Delete(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 {
|
func (_i *partnerContentController) Delete(c *fiber.Ctx) error {
|
||||||
idStr := c.Params("id")
|
idStr := c.Params("id")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ func (_i *PartnerContentsRouter) RegisterPartnerContentsRoutes() {
|
||||||
_i.App.Route("/partner-contents", func(router fiber.Router) {
|
_i.App.Route("/partner-contents", func(router fiber.Router) {
|
||||||
router.Get("/", partnerController.Show)
|
router.Get("/", partnerController.Show)
|
||||||
router.Post("/", partnerController.Save)
|
router.Post("/", partnerController.Save)
|
||||||
|
router.Post("/:id/logo", partnerController.UploadLogo)
|
||||||
router.Put("/:id", partnerController.Update)
|
router.Put("/:id", partnerController.Update)
|
||||||
router.Delete("/:id", partnerController.Delete)
|
router.Delete("/:id", partnerController.Delete)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package repository
|
package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
"web-qudo-be/app/database"
|
"web-qudo-be/app/database"
|
||||||
"web-qudo-be/app/database/entity"
|
"web-qudo-be/app/database/entity"
|
||||||
|
|
||||||
|
|
@ -17,6 +18,7 @@ type PartnerContentRepository interface {
|
||||||
Get() ([]entity.PartnerContent, error)
|
Get() ([]entity.PartnerContent, error)
|
||||||
Create(data *entity.PartnerContent) (*entity.PartnerContent, error)
|
Create(data *entity.PartnerContent) (*entity.PartnerContent, error)
|
||||||
Update(id uuid.UUID, data *entity.PartnerContent) error
|
Update(id uuid.UUID, data *entity.PartnerContent) error
|
||||||
|
UpdateImageFields(id uuid.UUID, imagePath, imageURL string) error
|
||||||
Delete(id uuid.UUID) error
|
Delete(id uuid.UUID) error
|
||||||
FindByID(id uuid.UUID) (*entity.PartnerContent, error) // opsional (buat soft delete)
|
FindByID(id uuid.UUID) (*entity.PartnerContent, error) // opsional (buat soft delete)
|
||||||
}
|
}
|
||||||
|
|
@ -58,7 +60,12 @@ func (r *partnerContentRepository) Update(id uuid.UUID, data *entity.PartnerCont
|
||||||
err := r.DB.DB.
|
err := r.DB.DB.
|
||||||
Model(&entity.PartnerContent{}).
|
Model(&entity.PartnerContent{}).
|
||||||
Where("id = ?", id).
|
Where("id = ?", id).
|
||||||
Updates(data).Error
|
Updates(map[string]interface{}{
|
||||||
|
"primary_title": data.PrimaryTitle,
|
||||||
|
"image_path": data.ImagePath,
|
||||||
|
"image_url": data.ImageURL,
|
||||||
|
"updated_at": time.Now(),
|
||||||
|
}).Error
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.Log.Error().Err(err).Msg("failed update partner content")
|
r.Log.Error().Err(err).Msg("failed update partner content")
|
||||||
|
|
@ -68,6 +75,21 @@ func (r *partnerContentRepository) Update(id uuid.UUID, data *entity.PartnerCont
|
||||||
return nil
|
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 {
|
func (r *partnerContentRepository) Delete(id uuid.UUID) error {
|
||||||
err := r.DB.DB.Delete(&entity.PartnerContent{}, id).Error
|
err := r.DB.DB.Delete(&entity.PartnerContent{}, id).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -90,4 +112,4 @@ func (r *partnerContentRepository) FindByID(id uuid.UUID) (*entity.PartnerConten
|
||||||
}
|
}
|
||||||
|
|
||||||
return &data, nil
|
return &data, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,44 @@
|
||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"mime/multipart"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
|
||||||
"web-qudo-be/app/database/entity"
|
"web-qudo-be/app/database/entity"
|
||||||
|
medialib "web-qudo-be/app/module/media_library/service"
|
||||||
"web-qudo-be/app/module/partner_contents/repository"
|
"web-qudo-be/app/module/partner_contents/repository"
|
||||||
|
minioStorage "web-qudo-be/config/config"
|
||||||
|
"web-qudo-be/utils/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
type partnerContentService struct {
|
type partnerContentService struct {
|
||||||
Repo repository.PartnerContentRepository
|
Repo repository.PartnerContentRepository
|
||||||
Log zerolog.Logger
|
MinioStorage *minioStorage.MinioStorage
|
||||||
|
MediaLib medialib.MediaLibraryService
|
||||||
|
Log zerolog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
type PartnerContentService interface {
|
type PartnerContentService interface {
|
||||||
Show() ([]entity.PartnerContent, error)
|
Show() ([]entity.PartnerContent, error)
|
||||||
Save(data *entity.PartnerContent) (*entity.PartnerContent, error)
|
Save(data *entity.PartnerContent) (*entity.PartnerContent, error)
|
||||||
Update(id uuid.UUID, data *entity.PartnerContent) error
|
Update(id uuid.UUID, data *entity.PartnerContent) error
|
||||||
|
UploadLogo(id uuid.UUID, file *multipart.FileHeader) error
|
||||||
Delete(id uuid.UUID) error
|
Delete(id uuid.UUID) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPartnerContentService(
|
func NewPartnerContentService(
|
||||||
repo repository.PartnerContentRepository,
|
repo repository.PartnerContentRepository,
|
||||||
|
minio *minioStorage.MinioStorage,
|
||||||
|
mediaLib medialib.MediaLibraryService,
|
||||||
log zerolog.Logger,
|
log zerolog.Logger,
|
||||||
) PartnerContentService {
|
) PartnerContentService {
|
||||||
return &partnerContentService{
|
return &partnerContentService{
|
||||||
Repo: repo,
|
Repo: repo,
|
||||||
Log: log,
|
MinioStorage: minio,
|
||||||
|
MediaLib: mediaLib,
|
||||||
|
Log: log,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -62,6 +74,20 @@ func (s *partnerContentService) Update(id uuid.UUID, data *entity.PartnerContent
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
if err := s.Repo.UpdateImageFields(id, key, url); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if s.MediaLib != nil {
|
||||||
|
_ = s.MediaLib.RegisterCMSAsset(url, key, "partner_logo", file)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *partnerContentService) Delete(id uuid.UUID) error {
|
func (s *partnerContentService) Delete(id uuid.UUID) error {
|
||||||
err := s.Repo.Delete(id)
|
err := s.Repo.Delete(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
|
|
||||||
|
|
@ -39,6 +40,32 @@ func NewPopupNewsContentImagesController(s service.PopupNewsContentImagesService
|
||||||
// @Failure 500 {object} response.Response
|
// @Failure 500 {object} response.Response
|
||||||
// @Router /popup-news-content-images [post]
|
// @Router /popup-news-content-images [post]
|
||||||
func (_i *popupNewsContentImagesController) Save(c *fiber.Ctx) error {
|
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)
|
req := new(request.PopupNewsContentImagesCreateRequest)
|
||||||
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -50,6 +77,7 @@ func (_i *popupNewsContentImagesController) Save(c *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
return utilRes.Resp(c, utilRes.Response{
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
|
Success: true,
|
||||||
Messages: utilRes.Messages{"Popup news content image successfully uploaded"},
|
Messages: utilRes.Messages{"Popup news content image successfully uploaded"},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,81 @@
|
||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"mime/multipart"
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
|
||||||
|
"web-qudo-be/app/database/entity"
|
||||||
|
medialib "web-qudo-be/app/module/media_library/service"
|
||||||
"web-qudo-be/app/module/popup_news_content_images/repository"
|
"web-qudo-be/app/module/popup_news_content_images/repository"
|
||||||
"web-qudo-be/app/module/popup_news_content_images/request"
|
"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 {
|
type popupNewsContentImagesService struct {
|
||||||
Repo repository.PopupNewsContentImagesRepository
|
Repo repository.PopupNewsContentImagesRepository
|
||||||
Log zerolog.Logger
|
MinioStorage *minioStorage.MinioStorage
|
||||||
|
MediaLib medialib.MediaLibraryService
|
||||||
|
Log zerolog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// interface
|
|
||||||
type PopupNewsContentImagesService interface {
|
type PopupNewsContentImagesService interface {
|
||||||
Save(req request.PopupNewsContentImagesCreateRequest) error
|
Save(req request.PopupNewsContentImagesCreateRequest) error
|
||||||
|
SaveWithFile(popupNewsContentID uint, file *multipart.FileHeader, isThumbnail *bool) (*entity.PopupNewsContentImages, error)
|
||||||
Delete(id uint) error
|
Delete(id uint) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// constructor
|
func NewPopupNewsContentImagesService(
|
||||||
func NewPopupNewsContentImagesService(repo repository.PopupNewsContentImagesRepository, log zerolog.Logger) PopupNewsContentImagesService {
|
repo repository.PopupNewsContentImagesRepository,
|
||||||
|
minio *minioStorage.MinioStorage,
|
||||||
|
log zerolog.Logger,
|
||||||
|
) PopupNewsContentImagesService {
|
||||||
return &popupNewsContentImagesService{
|
return &popupNewsContentImagesService{
|
||||||
Repo: repo,
|
Repo: repo,
|
||||||
Log: log,
|
MinioStorage: minio,
|
||||||
|
Log: log,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save
|
|
||||||
func (_i *popupNewsContentImagesService) Save(req request.PopupNewsContentImagesCreateRequest) error {
|
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())
|
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
|
||||||
|
}
|
||||||
|
if _i.MediaLib != nil {
|
||||||
|
_ = _i.MediaLib.RegisterCMSAsset(url, key, "popup_news", file)
|
||||||
|
}
|
||||||
|
return row, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (_i *popupNewsContentImagesService) Delete(id uint) error {
|
func (_i *popupNewsContentImagesService) Delete(id uint) error {
|
||||||
return _i.Repo.Delete(id)
|
return _i.Repo.Delete(id)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -110,13 +110,14 @@ func (_i *popupNewsContentsController) Save(c *fiber.Ctx) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err := _i.service.Save(*req)
|
data, err := _i.service.Save(*req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return utilRes.Resp(c, utilRes.Response{
|
return utilRes.Resp(c, utilRes.Response{
|
||||||
Messages: utilRes.Messages{"Popup news content successfully created"},
|
Messages: utilRes.Messages{"Popup news content successfully created"},
|
||||||
|
Data: data,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import (
|
||||||
"web-qudo-be/app/database/entity"
|
"web-qudo-be/app/database/entity"
|
||||||
"web-qudo-be/app/module/popup_news_contents/request"
|
"web-qudo-be/app/module/popup_news_contents/request"
|
||||||
"web-qudo-be/utils/paginator"
|
"web-qudo-be/utils/paginator"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type popupNewsContentsRepository struct {
|
type popupNewsContentsRepository struct {
|
||||||
|
|
@ -70,11 +72,22 @@ func (_i *popupNewsContentsRepository) Create(data *entity.PopupNewsContents) er
|
||||||
// Update
|
// Update
|
||||||
func (_i *popupNewsContentsRepository) Update(id uint, data *entity.PopupNewsContents) error {
|
func (_i *popupNewsContentsRepository) Update(id uint, data *entity.PopupNewsContents) error {
|
||||||
return _i.DB.DB.Model(&entity.PopupNewsContents{}).
|
return _i.DB.DB.Model(&entity.PopupNewsContents{}).
|
||||||
Where(&entity.PopupNewsContents{ID: id}).
|
Where("id = ?", id).
|
||||||
Updates(data).Error
|
Updates(map[string]interface{}{
|
||||||
|
"primary_title": data.PrimaryTitle,
|
||||||
|
"secondary_title": data.SecondaryTitle,
|
||||||
|
"description": data.Description,
|
||||||
|
"primary_cta": data.PrimaryCTA,
|
||||||
|
"secondary_cta_text": data.SecondaryCTAText,
|
||||||
|
}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete
|
// Delete removes child images first so FK constraints do not block the parent delete.
|
||||||
func (_i *popupNewsContentsRepository) Delete(id uint) error {
|
func (_i *popupNewsContentsRepository) Delete(id uint) error {
|
||||||
return _i.DB.DB.Delete(&entity.PopupNewsContents{}, id).Error
|
return _i.DB.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
}
|
if err := tx.Where("popup_news_content_id = ?", id).Delete(&entity.PopupNewsContentImages{}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return tx.Delete(&entity.PopupNewsContents{}, id).Error
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ type popupNewsContentsService struct {
|
||||||
type PopupNewsContentsService interface {
|
type PopupNewsContentsService interface {
|
||||||
All(req request.PopupNewsContentsQueryRequest) (data []*response.PopupNewsContentsResponse, paging paginator.Pagination, err error)
|
All(req request.PopupNewsContentsQueryRequest) (data []*response.PopupNewsContentsResponse, paging paginator.Pagination, err error)
|
||||||
Show(id uint) (*response.PopupNewsContentsResponse, error)
|
Show(id uint) (*response.PopupNewsContentsResponse, error)
|
||||||
Save(req request.PopupNewsContentsCreateRequest) error
|
Save(req request.PopupNewsContentsCreateRequest) (*response.PopupNewsContentsResponse, error)
|
||||||
Update(id uint, req request.PopupNewsContentsUpdateRequest) error
|
Update(id uint, req request.PopupNewsContentsUpdateRequest) error
|
||||||
Delete(id uint) error
|
Delete(id uint) error
|
||||||
}
|
}
|
||||||
|
|
@ -58,10 +58,14 @@ func (_i *popupNewsContentsService) Show(id uint) (*response.PopupNewsContentsRe
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save
|
// Save
|
||||||
func (_i *popupNewsContentsService) Save(req request.PopupNewsContentsCreateRequest) error {
|
func (_i *popupNewsContentsService) Save(req request.PopupNewsContentsCreateRequest) (*response.PopupNewsContentsResponse, error) {
|
||||||
_i.Log.Info().Interface("data", req).Msg("create popup news content")
|
_i.Log.Info().Interface("data", req).Msg("create popup news content")
|
||||||
|
|
||||||
return _i.Repo.Create(req.ToEntity())
|
ent := req.ToEntity()
|
||||||
|
if err := _i.Repo.Create(ent); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return mapper.PopupNewsContentsResponseMapper(ent), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update
|
// Update
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ import (
|
||||||
"web-qudo-be/app/module/cities"
|
"web-qudo-be/app/module/cities"
|
||||||
"web-qudo-be/app/module/client_approval_settings"
|
"web-qudo-be/app/module/client_approval_settings"
|
||||||
"web-qudo-be/app/module/clients"
|
"web-qudo-be/app/module/clients"
|
||||||
|
"web-qudo-be/app/module/cms_content_submissions"
|
||||||
|
"web-qudo-be/app/module/cms_media"
|
||||||
"web-qudo-be/app/module/custom_static_pages"
|
"web-qudo-be/app/module/custom_static_pages"
|
||||||
"web-qudo-be/app/module/districts"
|
"web-qudo-be/app/module/districts"
|
||||||
"web-qudo-be/app/module/feedbacks"
|
"web-qudo-be/app/module/feedbacks"
|
||||||
|
|
@ -27,6 +29,7 @@ import (
|
||||||
hero_content "web-qudo-be/app/module/hero_contents"
|
hero_content "web-qudo-be/app/module/hero_contents"
|
||||||
"web-qudo-be/app/module/magazine_files"
|
"web-qudo-be/app/module/magazine_files"
|
||||||
"web-qudo-be/app/module/magazines"
|
"web-qudo-be/app/module/magazines"
|
||||||
|
"web-qudo-be/app/module/media_library"
|
||||||
"web-qudo-be/app/module/master_menus"
|
"web-qudo-be/app/module/master_menus"
|
||||||
"web-qudo-be/app/module/master_modules"
|
"web-qudo-be/app/module/master_modules"
|
||||||
"web-qudo-be/app/module/our_product_content_images"
|
"web-qudo-be/app/module/our_product_content_images"
|
||||||
|
|
@ -72,13 +75,16 @@ type Router struct {
|
||||||
CitiesRouter *cities.CitiesRouter
|
CitiesRouter *cities.CitiesRouter
|
||||||
ClientApprovalSettingsRouter *client_approval_settings.ClientApprovalSettingsRouter
|
ClientApprovalSettingsRouter *client_approval_settings.ClientApprovalSettingsRouter
|
||||||
ClientsRouter *clients.ClientsRouter
|
ClientsRouter *clients.ClientsRouter
|
||||||
HeroContentsRouter *hero_content.HeroContentsRouter
|
CmsContentSubmissionsRouter *cms_content_submissions.CmsContentSubmissionsRouter
|
||||||
|
CmsMediaRouter *cms_media.CmsMediaRouter
|
||||||
|
HeroContentsRouter *hero_content.HeroContentsRouter
|
||||||
HeroContentImagesRouter *hero_content_image.HeroContentImagesRouter
|
HeroContentImagesRouter *hero_content_image.HeroContentImagesRouter
|
||||||
CustomStaticPagesRouter *custom_static_pages.CustomStaticPagesRouter
|
CustomStaticPagesRouter *custom_static_pages.CustomStaticPagesRouter
|
||||||
DistrictsRouter *districts.DistrictsRouter
|
DistrictsRouter *districts.DistrictsRouter
|
||||||
FeedbacksRouter *feedbacks.FeedbacksRouter
|
FeedbacksRouter *feedbacks.FeedbacksRouter
|
||||||
MagazineFilesRouter *magazine_files.MagazineFilesRouter
|
MagazineFilesRouter *magazine_files.MagazineFilesRouter
|
||||||
MagazinesRouter *magazines.MagazinesRouter
|
MagazinesRouter *magazines.MagazinesRouter
|
||||||
|
MediaLibraryRouter *media_library.MediaLibraryRouter
|
||||||
MasterMenusRouter *master_menus.MasterMenusRouter
|
MasterMenusRouter *master_menus.MasterMenusRouter
|
||||||
MasterModulesRouter *master_modules.MasterModulesRouter
|
MasterModulesRouter *master_modules.MasterModulesRouter
|
||||||
OurProductContentsRouter *our_product_contents.OurProductContentsRouter
|
OurProductContentsRouter *our_product_contents.OurProductContentsRouter
|
||||||
|
|
@ -119,6 +125,8 @@ func NewRouter(
|
||||||
citiesRouter *cities.CitiesRouter,
|
citiesRouter *cities.CitiesRouter,
|
||||||
clientApprovalSettingsRouter *client_approval_settings.ClientApprovalSettingsRouter,
|
clientApprovalSettingsRouter *client_approval_settings.ClientApprovalSettingsRouter,
|
||||||
clientsRouter *clients.ClientsRouter,
|
clientsRouter *clients.ClientsRouter,
|
||||||
|
cmsContentSubmissionsRouter *cms_content_submissions.CmsContentSubmissionsRouter,
|
||||||
|
cmsMediaRouter *cms_media.CmsMediaRouter,
|
||||||
heroContentsRouter *hero_content.HeroContentsRouter,
|
heroContentsRouter *hero_content.HeroContentsRouter,
|
||||||
heroContentImagesRouter *hero_content_image.HeroContentImagesRouter,
|
heroContentImagesRouter *hero_content_image.HeroContentImagesRouter,
|
||||||
customStaticPagesRouter *custom_static_pages.CustomStaticPagesRouter,
|
customStaticPagesRouter *custom_static_pages.CustomStaticPagesRouter,
|
||||||
|
|
@ -126,6 +134,7 @@ func NewRouter(
|
||||||
feedbacksRouter *feedbacks.FeedbacksRouter,
|
feedbacksRouter *feedbacks.FeedbacksRouter,
|
||||||
magazineFilesRouter *magazine_files.MagazineFilesRouter,
|
magazineFilesRouter *magazine_files.MagazineFilesRouter,
|
||||||
magazinesRouter *magazines.MagazinesRouter,
|
magazinesRouter *magazines.MagazinesRouter,
|
||||||
|
mediaLibraryRouter *media_library.MediaLibraryRouter,
|
||||||
masterMenuRouter *master_menus.MasterMenusRouter,
|
masterMenuRouter *master_menus.MasterMenusRouter,
|
||||||
masterModuleRouter *master_modules.MasterModulesRouter,
|
masterModuleRouter *master_modules.MasterModulesRouter,
|
||||||
ourProductContentsRouter *our_product_contents.OurProductContentsRouter,
|
ourProductContentsRouter *our_product_contents.OurProductContentsRouter,
|
||||||
|
|
@ -165,6 +174,8 @@ func NewRouter(
|
||||||
CitiesRouter: citiesRouter,
|
CitiesRouter: citiesRouter,
|
||||||
ClientApprovalSettingsRouter: clientApprovalSettingsRouter,
|
ClientApprovalSettingsRouter: clientApprovalSettingsRouter,
|
||||||
ClientsRouter: clientsRouter,
|
ClientsRouter: clientsRouter,
|
||||||
|
CmsContentSubmissionsRouter: cmsContentSubmissionsRouter,
|
||||||
|
CmsMediaRouter: cmsMediaRouter,
|
||||||
HeroContentsRouter: heroContentsRouter,
|
HeroContentsRouter: heroContentsRouter,
|
||||||
HeroContentImagesRouter: heroContentImagesRouter,
|
HeroContentImagesRouter: heroContentImagesRouter,
|
||||||
CustomStaticPagesRouter: customStaticPagesRouter,
|
CustomStaticPagesRouter: customStaticPagesRouter,
|
||||||
|
|
@ -172,6 +183,7 @@ func NewRouter(
|
||||||
FeedbacksRouter: feedbacksRouter,
|
FeedbacksRouter: feedbacksRouter,
|
||||||
MagazineFilesRouter: magazineFilesRouter,
|
MagazineFilesRouter: magazineFilesRouter,
|
||||||
MagazinesRouter: magazinesRouter,
|
MagazinesRouter: magazinesRouter,
|
||||||
|
MediaLibraryRouter: mediaLibraryRouter,
|
||||||
MasterMenusRouter: masterMenuRouter,
|
MasterMenusRouter: masterMenuRouter,
|
||||||
MasterModulesRouter: masterModuleRouter,
|
MasterModulesRouter: masterModuleRouter,
|
||||||
OurProductContentsRouter: ourProductContentsRouter,
|
OurProductContentsRouter: ourProductContentsRouter,
|
||||||
|
|
@ -221,6 +233,8 @@ func (r *Router) Register() {
|
||||||
r.CitiesRouter.RegisterCitiesRoutes()
|
r.CitiesRouter.RegisterCitiesRoutes()
|
||||||
r.ClientApprovalSettingsRouter.RegisterClientApprovalSettingsRoutes()
|
r.ClientApprovalSettingsRouter.RegisterClientApprovalSettingsRoutes()
|
||||||
r.ClientsRouter.RegisterClientsRoutes()
|
r.ClientsRouter.RegisterClientsRoutes()
|
||||||
|
r.CmsContentSubmissionsRouter.RegisterCmsContentSubmissionsRoutes()
|
||||||
|
r.CmsMediaRouter.RegisterCmsMediaRoutes()
|
||||||
r.HeroContentsRouter.RegisterHeroContentsRoutes()
|
r.HeroContentsRouter.RegisterHeroContentsRoutes()
|
||||||
r.HeroContentImagesRouter.RegisterHeroContentImagesRoutes()
|
r.HeroContentImagesRouter.RegisterHeroContentImagesRoutes()
|
||||||
r.CustomStaticPagesRouter.RegisterCustomStaticPagesRoutes()
|
r.CustomStaticPagesRouter.RegisterCustomStaticPagesRoutes()
|
||||||
|
|
@ -228,6 +242,7 @@ func (r *Router) Register() {
|
||||||
r.FeedbacksRouter.RegisterFeedbacksRoutes()
|
r.FeedbacksRouter.RegisterFeedbacksRoutes()
|
||||||
r.MagazinesRouter.RegisterMagazinesRoutes()
|
r.MagazinesRouter.RegisterMagazinesRoutes()
|
||||||
r.MagazineFilesRouter.RegisterMagazineFilesRoutes()
|
r.MagazineFilesRouter.RegisterMagazineFilesRoutes()
|
||||||
|
r.MediaLibraryRouter.RegisterMediaLibraryRoutes()
|
||||||
r.MasterMenusRouter.RegisterMasterMenusRoutes()
|
r.MasterMenusRouter.RegisterMasterMenusRoutes()
|
||||||
r.MasterModulesRouter.RegisterMasterModulesRoutes()
|
r.MasterModulesRouter.RegisterMasterModulesRoutes()
|
||||||
r.OurProductContentsRouter.RegisterOurProductContentsRoutes()
|
r.OurProductContentsRouter.RegisterOurProductContentsRoutes()
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,23 @@ type Config struct {
|
||||||
Smtp smtp
|
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
|
// NewConfig : initialize config
|
||||||
func NewConfig() *Config {
|
func NewConfig() *Config {
|
||||||
config, err := ParseConfig("config")
|
config, err := ParseConfig("config")
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/minio/minio-go/v7"
|
"github.com/minio/minio-go/v7"
|
||||||
|
|
@ -69,3 +70,13 @@ func (_minio *MinioStorage) ConnectMinio() (*minio.Client, error) {
|
||||||
log.Printf("[MinIO] Successfully connected to MinIO and bucket '%s' is ready", bucketName)
|
log.Printf("[MinIO] Successfully connected to MinIO and bucket '%s' is ready", bucketName)
|
||||||
return minioClient, nil
|
return minioClient, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PublicObjectURL builds a path-style URL for public reads (bucket policy must allow GET).
|
||||||
|
func (m *MinioStorage) PublicObjectURL(objectKey string) string {
|
||||||
|
o := m.Cfg.ObjectStorage.MinioStorage
|
||||||
|
scheme := "http"
|
||||||
|
if o.UseSSL {
|
||||||
|
scheme = "https"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s://%s/%s/%s", scheme, o.Endpoint, o.BucketName, objectKey)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,12 @@ external-port = ":8812"
|
||||||
idle-timeout = 5 # As seconds
|
idle-timeout = 5 # As seconds
|
||||||
print-routes = false
|
print-routes = false
|
||||||
prefork = false
|
prefork = false
|
||||||
production = false
|
# false: CMS preview URLs use http://localhost + port above. true: use domain (e.g. https://qudo.id/api).
|
||||||
|
production = true
|
||||||
body-limit = 1048576000 # "100 * 1024 * 1024"
|
body-limit = 1048576000 # "100 * 1024 * 1024"
|
||||||
|
|
||||||
[db.postgres]
|
[db.postgres]
|
||||||
dsn = "postgresql://medols_user:MedolsDB@2025@38.47.185.79:5432/medols_db" # <driver>://<username>:<password>@<host>:<port>/<database>
|
dsn = "postgresql://qudo_user:QudoDB@2026@38.47.185.79:5432/qudo_db" # <driver>://<username>:<password>@<host>:<port>/<database>
|
||||||
log-mode = "ERROR"
|
log-mode = "ERROR"
|
||||||
migrate = true
|
migrate = true
|
||||||
seed = false
|
seed = false
|
||||||
|
|
@ -28,7 +29,7 @@ endpoint = "is3.cloudhost.id"
|
||||||
access-key-id = "YRP1RM617986USRU6NN8"
|
access-key-id = "YRP1RM617986USRU6NN8"
|
||||||
secret-access-key = "vfbwQDYb1m7nfzo4LVEz90BIyOWfBMZ6bfGQbqDO"
|
secret-access-key = "vfbwQDYb1m7nfzo4LVEz90BIyOWfBMZ6bfGQbqDO"
|
||||||
use-ssl = true
|
use-ssl = true
|
||||||
bucket-name = "mikulnews"
|
bucket-name = "qudo"
|
||||||
location = "us-east-1"
|
location = "us-east-1"
|
||||||
|
|
||||||
[middleware.compress]
|
[middleware.compress]
|
||||||
|
|
|
||||||
13
docs/docs.go
13
docs/docs.go
|
|
@ -6869,6 +6869,12 @@ const docTemplate = `{
|
||||||
"name": "isPublish",
|
"name": "isPublish",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "myContentMode: \"own\" = current user's articles (any level); \"approver\" = non-draft from contributors (user_role_id 3) for approver history",
|
||||||
|
"name": "myContentMode",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"name": "source",
|
"name": "source",
|
||||||
|
|
@ -9908,6 +9914,11 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/cms-media/viewer/{path}": {
|
||||||
|
"get": {
|
||||||
|
"responses": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/custom-static-pages": {
|
"/custom-static-pages": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
|
|
@ -17124,7 +17135,6 @@ const docTemplate = `{
|
||||||
"request.ArticlesCreateRequest": {
|
"request.ArticlesCreateRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"categoryIds",
|
|
||||||
"description",
|
"description",
|
||||||
"htmlDescription",
|
"htmlDescription",
|
||||||
"slug",
|
"slug",
|
||||||
|
|
@ -17183,7 +17193,6 @@ const docTemplate = `{
|
||||||
"request.ArticlesUpdateRequest": {
|
"request.ArticlesUpdateRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"categoryIds",
|
|
||||||
"description",
|
"description",
|
||||||
"htmlDescription",
|
"htmlDescription",
|
||||||
"slug",
|
"slug",
|
||||||
|
|
|
||||||
|
|
@ -6858,6 +6858,12 @@
|
||||||
"name": "isPublish",
|
"name": "isPublish",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "myContentMode: \"own\" = current user's articles (any level); \"approver\" = non-draft from contributors (user_role_id 3) for approver history",
|
||||||
|
"name": "myContentMode",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"name": "source",
|
"name": "source",
|
||||||
|
|
@ -9897,6 +9903,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/cms-media/viewer/{path}": {
|
||||||
|
"get": {
|
||||||
|
"responses": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/custom-static-pages": {
|
"/custom-static-pages": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
|
|
@ -17113,7 +17124,6 @@
|
||||||
"request.ArticlesCreateRequest": {
|
"request.ArticlesCreateRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"categoryIds",
|
|
||||||
"description",
|
"description",
|
||||||
"htmlDescription",
|
"htmlDescription",
|
||||||
"slug",
|
"slug",
|
||||||
|
|
@ -17172,7 +17182,6 @@
|
||||||
"request.ArticlesUpdateRequest": {
|
"request.ArticlesUpdateRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"categoryIds",
|
|
||||||
"description",
|
"description",
|
||||||
"htmlDescription",
|
"htmlDescription",
|
||||||
"slug",
|
"slug",
|
||||||
|
|
|
||||||
|
|
@ -488,7 +488,6 @@ definitions:
|
||||||
typeId:
|
typeId:
|
||||||
type: integer
|
type: integer
|
||||||
required:
|
required:
|
||||||
- categoryIds
|
|
||||||
- description
|
- description
|
||||||
- htmlDescription
|
- htmlDescription
|
||||||
- slug
|
- slug
|
||||||
|
|
@ -529,7 +528,6 @@ definitions:
|
||||||
typeId:
|
typeId:
|
||||||
type: integer
|
type: integer
|
||||||
required:
|
required:
|
||||||
- categoryIds
|
|
||||||
- description
|
- description
|
||||||
- htmlDescription
|
- htmlDescription
|
||||||
- slug
|
- slug
|
||||||
|
|
@ -5840,6 +5838,11 @@ paths:
|
||||||
- in: query
|
- in: query
|
||||||
name: isPublish
|
name: isPublish
|
||||||
type: boolean
|
type: boolean
|
||||||
|
- description: 'myContentMode: "own" = current user''s articles (any level);
|
||||||
|
"approver" = non-draft from contributors (user_role_id 3) for approver history'
|
||||||
|
in: query
|
||||||
|
name: myContentMode
|
||||||
|
type: string
|
||||||
- in: query
|
- in: query
|
||||||
name: source
|
name: source
|
||||||
type: string
|
type: string
|
||||||
|
|
@ -7793,6 +7796,9 @@ paths:
|
||||||
summary: update Clients
|
summary: update Clients
|
||||||
tags:
|
tags:
|
||||||
- Clients
|
- Clients
|
||||||
|
/cms-media/viewer/{path}:
|
||||||
|
get:
|
||||||
|
responses: {}
|
||||||
/custom-static-pages:
|
/custom-static-pages:
|
||||||
get:
|
get:
|
||||||
description: API for getting all CustomStaticPages
|
description: API for getting all CustomStaticPages
|
||||||
|
|
|
||||||
6
main.go
6
main.go
|
|
@ -23,6 +23,8 @@ import (
|
||||||
"web-qudo-be/app/module/cities"
|
"web-qudo-be/app/module/cities"
|
||||||
"web-qudo-be/app/module/client_approval_settings"
|
"web-qudo-be/app/module/client_approval_settings"
|
||||||
"web-qudo-be/app/module/clients"
|
"web-qudo-be/app/module/clients"
|
||||||
|
"web-qudo-be/app/module/cms_content_submissions"
|
||||||
|
"web-qudo-be/app/module/cms_media"
|
||||||
"web-qudo-be/app/module/custom_static_pages"
|
"web-qudo-be/app/module/custom_static_pages"
|
||||||
"web-qudo-be/app/module/districts"
|
"web-qudo-be/app/module/districts"
|
||||||
"web-qudo-be/app/module/feedbacks"
|
"web-qudo-be/app/module/feedbacks"
|
||||||
|
|
@ -30,6 +32,7 @@ import (
|
||||||
hero_content "web-qudo-be/app/module/hero_contents"
|
hero_content "web-qudo-be/app/module/hero_contents"
|
||||||
"web-qudo-be/app/module/magazine_files"
|
"web-qudo-be/app/module/magazine_files"
|
||||||
"web-qudo-be/app/module/magazines"
|
"web-qudo-be/app/module/magazines"
|
||||||
|
"web-qudo-be/app/module/media_library"
|
||||||
"web-qudo-be/app/module/master_menus"
|
"web-qudo-be/app/module/master_menus"
|
||||||
"web-qudo-be/app/module/master_modules"
|
"web-qudo-be/app/module/master_modules"
|
||||||
"web-qudo-be/app/module/our_product_content_images"
|
"web-qudo-be/app/module/our_product_content_images"
|
||||||
|
|
@ -104,6 +107,8 @@ func main() {
|
||||||
cities.NewCitiesModule,
|
cities.NewCitiesModule,
|
||||||
client_approval_settings.NewClientApprovalSettingsModule,
|
client_approval_settings.NewClientApprovalSettingsModule,
|
||||||
clients.NewClientsModule,
|
clients.NewClientsModule,
|
||||||
|
cms_content_submissions.NewCmsContentSubmissionsModule,
|
||||||
|
cms_media.NewCmsMediaModule,
|
||||||
custom_static_pages.NewCustomStaticPagesModule,
|
custom_static_pages.NewCustomStaticPagesModule,
|
||||||
districts.NewDistrictsModule,
|
districts.NewDistrictsModule,
|
||||||
feedbacks.NewFeedbacksModule,
|
feedbacks.NewFeedbacksModule,
|
||||||
|
|
@ -111,6 +116,7 @@ func main() {
|
||||||
hero_content_image.NewHeroContentImagesModule,
|
hero_content_image.NewHeroContentImagesModule,
|
||||||
magazines.NewMagazinesModule,
|
magazines.NewMagazinesModule,
|
||||||
magazine_files.NewMagazineFilesModule,
|
magazine_files.NewMagazineFilesModule,
|
||||||
|
media_library.NewMediaLibraryModule,
|
||||||
master_menus.NewMasterMenusModule,
|
master_menus.NewMasterMenusModule,
|
||||||
master_modules.NewMasterModulesModule,
|
master_modules.NewMasterModulesModule,
|
||||||
our_product_contents.NewOurProductContentsModule,
|
our_product_contents.NewOurProductContentsModule,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"mime"
|
||||||
|
"mime/multipart"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/minio/minio-go/v7"
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
var mediaExts = map[string]bool{
|
||||||
|
".jpg": true, ".jpeg": true, ".png": true, ".gif": true, ".webp": true,
|
||||||
|
".mp4": true, ".webm": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// mediaLibraryExts = images + video + audio + common documents (admin Media Library upload).
|
||||||
|
var mediaLibraryExts = map[string]bool{
|
||||||
|
".jpg": true, ".jpeg": true, ".png": true, ".gif": true, ".webp": true, ".svg": true,
|
||||||
|
".mp4": true, ".webm": true, ".mov": true,
|
||||||
|
".mp3": true, ".wav": true, ".ogg": true, ".m4a": true,
|
||||||
|
".pdf": true, ".doc": true, ".docx": true, ".txt": true, ".csv": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
ext := strings.ToLower(filepath.Ext(file.Filename))
|
||||||
|
if folder == "media-library" {
|
||||||
|
if !mediaLibraryExts[ext] {
|
||||||
|
return "", "", fmt.Errorf("unsupported file type for media library")
|
||||||
|
}
|
||||||
|
} else if allowVideo {
|
||||||
|
if !mediaExts[ext] {
|
||||||
|
return "", "", fmt.Errorf("unsupported file type (allowed: images, mp4, webm)")
|
||||||
|
}
|
||||||
|
} else if !imageExts[ext] {
|
||||||
|
return "", "", fmt.Errorf("unsupported image type")
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := ms.ConnectMinio()
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
src, err := file.Open()
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
defer src.Close()
|
||||||
|
|
||||||
|
bucket := ms.Cfg.ObjectStorage.MinioStorage.BucketName
|
||||||
|
now := time.Now()
|
||||||
|
objectKey = fmt.Sprintf("cms/%s/%d/%02d/%s%s", folder, now.Year(), int(now.Month()), uuid.New().String(), ext)
|
||||||
|
|
||||||
|
contentType := mime.TypeByExtension(ext)
|
||||||
|
if contentType == "" {
|
||||||
|
contentType = "application/octet-stream"
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = client.PutObject(context.Background(), bucket, objectKey, src, file.Size, minio.PutObjectOptions{
|
||||||
|
ContentType: contentType,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return objectKey, CMSPreviewURL(ms.Cfg, objectKey), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UploadMediaLibraryObject stores under cms/media-library/... with a broader MIME allowlist.
|
||||||
|
func UploadMediaLibraryObject(ms *appcfg.MinioStorage, file *multipart.FileHeader) (objectKey string, previewURL string, err error) {
|
||||||
|
if file == nil {
|
||||||
|
return "", "", fmt.Errorf("file is required")
|
||||||
|
}
|
||||||
|
ext := strings.ToLower(filepath.Ext(file.Filename))
|
||||||
|
if !mediaLibraryExts[ext] {
|
||||||
|
return "", "", fmt.Errorf("unsupported file type for media library")
|
||||||
|
}
|
||||||
|
return UploadCMSObject(ms, "media-library", file, false)
|
||||||
|
}
|
||||||
BIN
web-qudo-be.exe
BIN
web-qudo-be.exe
Binary file not shown.
Loading…
Reference in New Issue