package controllers import ( "errors" "net/http" "strconv" "strings" "github.com/gin-gonic/gin" "gorm.io/gorm" blogModels "goaresv3/app/blog/models" "goaresv3/config" ) type UpsertCategoryRequest struct { Title string `json:"title" binding:"required,max=254"` Slug string `json:"slug" binding:"required,max=254"` Description string `json:"description"` ParentID *uint `json:"parent_id"` } type UpsertTagRequest struct { Name string `json:"name" binding:"required,max=254"` } type UpsertPostRequest struct { Title string `json:"title" binding:"required,max=254"` Images string `json:"images" binding:"required"` ImagesMid string `json:"images_mid" binding:"required"` ImagesMin string `json:"images_min" binding:"required"` Width int `json:"width"` Height int `json:"height"` Quality int `json:"quality"` Format string `json:"format" binding:"omitempty,max=10"` Content string `json:"content"` Slug string `json:"slug" binding:"required,max=254"` CategoryIDs []uint `json:"category_ids"` TagIDs []uint `json:"tag_ids"` } func parseBlogID(c *gin.Context) (uint, bool) { id, err := strconv.ParseUint(strings.TrimSpace(c.Param("id")), 10, 64) if err != nil || id == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) return 0, false } return uint(id), true } // ListCategories godoc // @Summary List blog categories // @Tags Blog // @Produce json // @Success 200 {array} map[string]interface{} // @Failure 500 {object} map[string]string // @Router /api/v1/blog/categories [get] func ListCategories(c *gin.Context) { var items []blogModels.Category if err := config.DB.Preload("Children").Order("id DESC").Find(&items).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch categories"}) return } c.JSON(http.StatusOK, items) } // CreateCategory godoc // @Summary Create blog category // @Tags Blog // @Security BearerAuth // @Accept json // @Produce json // @Param request body UpsertCategoryRequest true "category payload" // @Success 201 {object} map[string]interface{} // @Failure 400 {object} map[string]string // @Failure 409 {object} map[string]string // @Router /api/v1/blog/categories [post] func CreateCategory(c *gin.Context) { var req UpsertCategoryRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } item := blogModels.Category{ Title: req.Title, Slug: req.Slug, Description: req.Description, ParentID: req.ParentID, } if err := config.DB.Create(&item).Error; err != nil { c.JSON(http.StatusConflict, gin.H{"error": "failed to create category"}) return } c.JSON(http.StatusCreated, item) } // UpdateCategory godoc // @Summary Update blog category // @Tags Blog // @Security BearerAuth // @Accept json // @Produce json // @Param id path int true "category id" // @Param request body UpsertCategoryRequest true "category payload" // @Success 200 {object} map[string]interface{} // @Failure 400 {object} map[string]string // @Failure 404 {object} map[string]string // @Failure 409 {object} map[string]string // @Router /api/v1/blog/categories/{id} [put] func UpdateCategory(c *gin.Context) { id, ok := parseBlogID(c) if !ok { return } var req UpsertCategoryRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } var item blogModels.Category if err := config.DB.First(&item, id).Error; errors.Is(err, gorm.ErrRecordNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": "category not found"}) return } if err := config.DB.Model(&item).Updates(map[string]any{ "title": req.Title, "slug": req.Slug, "description": req.Description, "parent_id": req.ParentID, }).Error; err != nil { c.JSON(http.StatusConflict, gin.H{"error": "failed to update category"}) return } _ = config.DB.First(&item, id).Error c.JSON(http.StatusOK, item) } // DeleteCategory godoc // @Summary Delete blog category // @Tags Blog // @Security BearerAuth // @Produce json // @Param id path int true "category id" // @Success 200 {object} map[string]string // @Failure 400 {object} map[string]string // @Failure 404 {object} map[string]string // @Router /api/v1/blog/categories/{id} [delete] func DeleteCategory(c *gin.Context) { id, ok := parseBlogID(c) if !ok { return } res := config.DB.Delete(&blogModels.Category{}, id) if res.RowsAffected == 0 { c.JSON(http.StatusNotFound, gin.H{"error": "category not found"}) return } c.JSON(http.StatusOK, gin.H{"message": "category deleted"}) } // ListTags godoc // @Summary List blog tags // @Tags Blog // @Produce json // @Success 200 {array} map[string]interface{} // @Router /api/v1/blog/tags [get] func ListTags(c *gin.Context) { var items []blogModels.Tag if err := config.DB.Order("id DESC").Find(&items).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch tags"}) return } c.JSON(http.StatusOK, items) } // CreateTag godoc // @Summary Create blog tag // @Tags Blog // @Security BearerAuth // @Accept json // @Produce json // @Param request body UpsertTagRequest true "tag payload" // @Success 201 {object} map[string]interface{} // @Router /api/v1/blog/tags [post] func CreateTag(c *gin.Context) { var req UpsertTagRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } item := blogModels.Tag{Name: req.Name} if err := config.DB.Create(&item).Error; err != nil { c.JSON(http.StatusConflict, gin.H{"error": "failed to create tag"}) return } c.JSON(http.StatusCreated, item) } // UpdateTag godoc // @Summary Update blog tag // @Tags Blog // @Security BearerAuth // @Accept json // @Produce json // @Param id path int true "tag id" // @Param request body UpsertTagRequest true "tag payload" // @Success 200 {object} map[string]interface{} // @Router /api/v1/blog/tags/{id} [put] func UpdateTag(c *gin.Context) { id, ok := parseBlogID(c) if !ok { return } var req UpsertTagRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } var item blogModels.Tag if err := config.DB.First(&item, id).Error; errors.Is(err, gorm.ErrRecordNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": "tag not found"}) return } if err := config.DB.Model(&item).Update("name", req.Name).Error; err != nil { c.JSON(http.StatusConflict, gin.H{"error": "failed to update tag"}) return } _ = config.DB.First(&item, id).Error c.JSON(http.StatusOK, item) } // DeleteTag godoc // @Summary Delete blog tag // @Tags Blog // @Security BearerAuth // @Produce json // @Param id path int true "tag id" // @Success 200 {object} map[string]string // @Router /api/v1/blog/tags/{id} [delete] func DeleteTag(c *gin.Context) { id, ok := parseBlogID(c) if !ok { return } res := config.DB.Delete(&blogModels.Tag{}, id) if res.RowsAffected == 0 { c.JSON(http.StatusNotFound, gin.H{"error": "tag not found"}) return } c.JSON(http.StatusOK, gin.H{"message": "tag deleted"}) } // ListPosts godoc // @Summary List blog posts // @Tags Blog // @Produce json // @Success 200 {array} map[string]interface{} // @Router /api/v1/blog/posts [get] func ListPosts(c *gin.Context) { var items []blogModels.Post if err := config.DB.Preload("Categories").Preload("Tags").Order("id DESC").Find(&items).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch posts"}) return } c.JSON(http.StatusOK, items) } // GetPost godoc // @Summary Get blog post // @Tags Blog // @Produce json // @Param id path int true "post id" // @Success 200 {object} map[string]interface{} // @Failure 404 {object} map[string]string // @Router /api/v1/blog/posts/{id} [get] func GetPost(c *gin.Context) { id, ok := parseBlogID(c) if !ok { return } var item blogModels.Post if err := config.DB.Preload("Categories").Preload("Tags").First(&item, id).Error; errors.Is(err, gorm.ErrRecordNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": "post not found"}) return } c.JSON(http.StatusOK, item) } // CreatePost godoc // @Summary Create blog post // @Tags Blog // @Security BearerAuth // @Accept json // @Produce json // @Param request body UpsertPostRequest true "post payload" // @Success 201 {object} map[string]interface{} // @Router /api/v1/blog/posts [post] func CreatePost(c *gin.Context) { var req UpsertPostRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } item := blogModels.Post{ Title: req.Title, Images: req.Images, ImagesMid: req.ImagesMid, ImagesMin: req.ImagesMin, Width: req.Width, Height: req.Height, Quality: req.Quality, Format: req.Format, Content: req.Content, Slug: req.Slug, } if err := config.DB.Create(&item).Error; err != nil { c.JSON(http.StatusConflict, gin.H{"error": "failed to create post"}) return } if err := assignPostRelations(item.ID, req.CategoryIDs, req.TagIDs); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to assign relations"}) return } _ = config.DB.Preload("Categories").Preload("Tags").First(&item, item.ID).Error c.JSON(http.StatusCreated, item) } // UpdatePost godoc // @Summary Update blog post // @Tags Blog // @Security BearerAuth // @Accept json // @Produce json // @Param id path int true "post id" // @Param request body UpsertPostRequest true "post payload" // @Success 200 {object} map[string]interface{} // @Router /api/v1/blog/posts/{id} [put] func UpdatePost(c *gin.Context) { id, ok := parseBlogID(c) if !ok { return } var req UpsertPostRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } var item blogModels.Post if err := config.DB.First(&item, id).Error; errors.Is(err, gorm.ErrRecordNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": "post not found"}) return } if err := config.DB.Model(&item).Updates(map[string]any{ "title": req.Title, "images": req.Images, "images_mid": req.ImagesMid, "images_min": req.ImagesMin, "width": req.Width, "height": req.Height, "quality": req.Quality, "format": req.Format, "content": req.Content, "slug": req.Slug, }).Error; err != nil { c.JSON(http.StatusConflict, gin.H{"error": "failed to update post"}) return } if err := assignPostRelations(item.ID, req.CategoryIDs, req.TagIDs); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to assign relations"}) return } _ = config.DB.Preload("Categories").Preload("Tags").First(&item, item.ID).Error c.JSON(http.StatusOK, item) } // DeletePost godoc // @Summary Delete blog post // @Tags Blog // @Security BearerAuth // @Produce json // @Param id path int true "post id" // @Success 200 {object} map[string]string // @Router /api/v1/blog/posts/{id} [delete] func DeletePost(c *gin.Context) { id, ok := parseBlogID(c) if !ok { return } res := config.DB.Delete(&blogModels.Post{}, id) if res.RowsAffected == 0 { c.JSON(http.StatusNotFound, gin.H{"error": "post not found"}) return } c.JSON(http.StatusOK, gin.H{"message": "post deleted"}) } func assignPostRelations(postID uint, categoryIDs, tagIDs []uint) error { var p blogModels.Post if err := config.DB.First(&p, postID).Error; err != nil { return err } if categoryIDs != nil { var categories []blogModels.Category if len(categoryIDs) > 0 { if err := config.DB.Where("id IN ?", categoryIDs).Find(&categories).Error; err != nil { return err } } if err := config.DB.Model(&p).Association("Categories").Replace(categories); err != nil { return err } } if tagIDs != nil { var tags []blogModels.Tag if len(tagIDs) > 0 { if err := config.DB.Where("id IN ?", tagIDs).Find(&tags).Error; err != nil { return err } } if err := config.DB.Model(&p).Association("Tags").Replace(tags); err != nil { return err } } return nil }