462 lines
12 KiB
Go
462 lines
12 KiB
Go
package handlers
|
|
|
|
import (
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"gauth-central/config"
|
|
"gauth-central/internal/services"
|
|
"gauth-central/pkg/utils"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
type PostHandler struct {
|
|
postService *services.PostService
|
|
}
|
|
|
|
func NewPostHandler(postService *services.PostService) *PostHandler {
|
|
return &PostHandler{postService: postService}
|
|
}
|
|
|
|
// GetAllPosts godoc
|
|
// @Summary Get all active posts with pagination
|
|
// @Description Retrieve a list of active posts with pagination. Use front=true for front posts only
|
|
// @Tags posts
|
|
// @Produce json
|
|
// @Param front query bool false "Front posts only"
|
|
// @Param page query int false "Page number" default(1)
|
|
// @Param limit query int false "Items per page" default(10)
|
|
// @Success 200 {object} map[string]interface{}
|
|
// @Failure 500 {object} map[string]string
|
|
// @Router /posts [get]
|
|
// GetAllPosts returns active posts with pagination. Optional query: front=true for front posts only.
|
|
func (h *PostHandler) GetAllPosts(c *gin.Context) {
|
|
onlyFront := false
|
|
if raw := strings.TrimSpace(c.Query("front")); raw != "" {
|
|
parsed, err := strconv.ParseBool(raw)
|
|
if err == nil {
|
|
onlyFront = parsed
|
|
}
|
|
}
|
|
|
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
|
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
|
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
if limit < 1 || limit > 100 {
|
|
limit = 10
|
|
}
|
|
|
|
items, total, err := h.postService.GetAllPosts(true, onlyFront, page, limit)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"data": items,
|
|
"total": total,
|
|
"page": page,
|
|
"limit": limit,
|
|
})
|
|
}
|
|
|
|
// GetPostBySlug godoc
|
|
// @Summary Get post by slug
|
|
// @Description Retrieve an active post by slug
|
|
// @Tags posts
|
|
// @Produce json
|
|
// @Param slug path string true "Post Slug"
|
|
// @Success 200 {object} models.Post
|
|
// @Failure 404 {object} map[string]string
|
|
// @Router /posts/slug/{slug} [get]
|
|
// GetPostBySlug returns a post by slug.
|
|
func (h *PostHandler) GetPostBySlug(c *gin.Context) {
|
|
slug := c.Param("slug")
|
|
item, err := h.postService.GetPostBySlug(slug, true)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, item)
|
|
}
|
|
|
|
// AdminGetAllPosts godoc
|
|
// @Summary Get all posts (Admin)
|
|
// @Description Retrieve a list of all posts including inactive ones with pagination
|
|
// @Tags admin
|
|
// @Security ApiKeyAuth
|
|
// @Produce json
|
|
// @Param page query int false "Page number" default(1)
|
|
// @Param limit query int false "Items per page" default(10)
|
|
// @Success 200 {object} map[string]interface{}
|
|
// @Failure 500 {object} map[string]string
|
|
// @Router /admin/posts [get]
|
|
// AdminGetAllPosts returns all posts with pagination.
|
|
func (h *PostHandler) AdminGetAllPosts(c *gin.Context) {
|
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
|
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
|
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
if limit < 1 || limit > 100 {
|
|
limit = 10
|
|
}
|
|
|
|
items, total, err := h.postService.GetAllPosts(false, false, page, limit)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"data": items,
|
|
"total": total,
|
|
"page": page,
|
|
"limit": limit,
|
|
})
|
|
}
|
|
|
|
// AdminGetPostByID godoc
|
|
// @Summary Get post by ID (Admin)
|
|
// @Description Retrieve a post by ID
|
|
// @Tags admin
|
|
// @Security ApiKeyAuth
|
|
// @Produce json
|
|
// @Param id path string true "Post ID"
|
|
// @Success 200 {object} models.Post
|
|
// @Failure 404 {object} map[string]string
|
|
// @Router /admin/posts/{id} [get]
|
|
// AdminGetPostByID returns a post by ID.
|
|
func (h *PostHandler) AdminGetPostByID(c *gin.Context) {
|
|
id := c.Param("id")
|
|
item, err := h.postService.GetPostByID(id)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, item)
|
|
}
|
|
|
|
// CreatePost godoc
|
|
// @Summary Create a post (Admin)
|
|
// @Description Create a new post
|
|
// @Tags admin
|
|
// @Security ApiKeyAuth
|
|
// @Accept multipart/form-data
|
|
// @Produce json
|
|
// @Param title formData string true "Title"
|
|
// @Param content formData string false "Content"
|
|
// @Param keywords formData string true "Keywords"
|
|
// @Param video formData string false "Video"
|
|
// @Param category_ids formData []string false "Category IDs"
|
|
// @Param tag_ids formData []string false "Tag IDs"
|
|
// @Param parent_id formData string false "Parent ID"
|
|
// @Param image formData file false "Image"
|
|
// @Param is_active formData bool false "Is active"
|
|
// @Param is_front formData bool false "Is front"
|
|
// @Success 201 {object} models.Post
|
|
// @Failure 400 {object} map[string]string
|
|
// @Router /admin/posts [post]
|
|
// CreatePost creates a new post.
|
|
func (h *PostHandler) CreatePost(c *gin.Context) {
|
|
title := strings.TrimSpace(c.PostForm("title"))
|
|
content := strings.TrimSpace(c.PostForm("content"))
|
|
keywords := strings.TrimSpace(c.PostForm("keywords"))
|
|
video := strings.TrimSpace(c.PostForm("video"))
|
|
if title == "" || keywords == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "title and keywords are required"})
|
|
return
|
|
}
|
|
if video == "" {
|
|
video = "none"
|
|
}
|
|
|
|
parentID, err := parseUUIDPtr(c.PostForm("parent_id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid parent_id"})
|
|
return
|
|
}
|
|
|
|
isActive := true
|
|
isActivePtr, err := parseOptionalBool(c, "is_active")
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "is_active must be true or false"})
|
|
return
|
|
}
|
|
if isActivePtr != nil {
|
|
isActive = *isActivePtr
|
|
}
|
|
|
|
isFront := true
|
|
isFrontPtr, err := parseOptionalBool(c, "is_front")
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "is_front must be true or false"})
|
|
return
|
|
}
|
|
if isFrontPtr != nil {
|
|
isFront = *isFrontPtr
|
|
}
|
|
|
|
categoryIDs := parseIDList(c, "category_ids")
|
|
tagIDs := parseIDList(c, "tag_ids")
|
|
|
|
imageURL := ""
|
|
file, fileErr := c.FormFile("image")
|
|
if fileErr == nil {
|
|
if file.Size > 5*1024*1024 {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Image size exceeds 5MB limit"})
|
|
return
|
|
}
|
|
|
|
saved, saveErr := utils.SaveOptimizedImage(file, "./uploads/posts", "post", &utils.ImageOptions{
|
|
Width: config.AppConfig.PostImageWidth,
|
|
Height: config.AppConfig.PostImageHeight,
|
|
Quality: float32(config.AppConfig.PostImageQuality),
|
|
Format: config.AppConfig.PostImageFormat,
|
|
Mode: config.AppConfig.PostImageMode,
|
|
})
|
|
if saveErr != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save image: " + saveErr.Error()})
|
|
return
|
|
}
|
|
imageURL = saved
|
|
}
|
|
|
|
item, err := h.postService.CreatePost(
|
|
title,
|
|
content,
|
|
keywords,
|
|
imageURL,
|
|
video,
|
|
categoryIDs,
|
|
tagIDs,
|
|
parentID,
|
|
isActive,
|
|
isFront,
|
|
)
|
|
if err != nil {
|
|
status := http.StatusInternalServerError
|
|
switch err.Error() {
|
|
case "slug cannot be empty", "slug already exists", "one or more categories not found", "one or more tags not found":
|
|
status = http.StatusBadRequest
|
|
}
|
|
c.JSON(status, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, item)
|
|
}
|
|
|
|
// UpdatePost godoc
|
|
// @Summary Update a post (Admin)
|
|
// @Description Update an existing post
|
|
// @Tags admin
|
|
// @Security ApiKeyAuth
|
|
// @Accept multipart/form-data
|
|
// @Produce json
|
|
// @Param id path string true "Post ID"
|
|
// @Param title formData string false "Title"
|
|
// @Param content formData string false "Content"
|
|
// @Param keywords formData string false "Keywords"
|
|
// @Param video formData string false "Video"
|
|
// @Param category_ids formData []string false "Category IDs"
|
|
// @Param tag_ids formData []string false "Tag IDs"
|
|
// @Param parent_id formData string false "Parent ID"
|
|
// @Param image formData file false "Image"
|
|
// @Param slug formData string false "Slug"
|
|
// @Param is_active formData bool false "Is active"
|
|
// @Param is_front formData bool false "Is front"
|
|
// @Success 200 {object} models.Post
|
|
// @Failure 400 {object} map[string]string
|
|
// @Failure 404 {object} map[string]string
|
|
// @Router /admin/posts/{id} [put]
|
|
// UpdatePost updates a post.
|
|
func (h *PostHandler) UpdatePost(c *gin.Context) {
|
|
id := c.Param("id")
|
|
|
|
title, hasTitle := getOptionalFormValue(c, "title")
|
|
content, hasContent := getOptionalFormValue(c, "content")
|
|
keywords, hasKeywords := getOptionalFormValue(c, "keywords")
|
|
video, hasVideo := getOptionalFormValue(c, "video")
|
|
slug, hasSlug := getOptionalFormValue(c, "slug")
|
|
|
|
var titlePtr *string
|
|
var contentPtr *string
|
|
var keywordsPtr *string
|
|
var videoPtr *string
|
|
var slugPtr *string
|
|
|
|
if hasTitle {
|
|
titlePtr = &title
|
|
}
|
|
if hasContent {
|
|
contentPtr = &content
|
|
}
|
|
if hasKeywords {
|
|
keywordsPtr = &keywords
|
|
}
|
|
if hasVideo {
|
|
videoPtr = &video
|
|
}
|
|
if hasSlug {
|
|
slugPtr = &slug
|
|
}
|
|
|
|
parentIDValue, hasParent := getOptionalFormValue(c, "parent_id")
|
|
parentIDSet := false
|
|
var parentIDPtr *uuid.UUID
|
|
if hasParent {
|
|
parentIDSet = true
|
|
if parentIDValue == "" {
|
|
parentIDPtr = nil
|
|
} else {
|
|
parsed, err := parseUUIDPtr(parentIDValue)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid parent_id"})
|
|
return
|
|
}
|
|
parentIDPtr = parsed
|
|
}
|
|
}
|
|
|
|
isActivePtr, err := parseOptionalBool(c, "is_active")
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "is_active must be true or false"})
|
|
return
|
|
}
|
|
|
|
isFrontPtr, err := parseOptionalBool(c, "is_front")
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "is_front must be true or false"})
|
|
return
|
|
}
|
|
|
|
var categoryIDsPtr *[]string
|
|
if hasIDList(c, "category_ids") {
|
|
ids := parseIDList(c, "category_ids")
|
|
categoryIDsPtr = &ids
|
|
}
|
|
|
|
var tagIDsPtr *[]string
|
|
if hasIDList(c, "tag_ids") {
|
|
ids := parseIDList(c, "tag_ids")
|
|
tagIDsPtr = &ids
|
|
}
|
|
|
|
var imagePtr *string
|
|
file, fileErr := c.FormFile("image")
|
|
if fileErr == nil {
|
|
if file.Size > 5*1024*1024 {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Image size exceeds 5MB limit"})
|
|
return
|
|
}
|
|
|
|
item, fetchErr := h.postService.GetPostByID(id)
|
|
if fetchErr != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": fetchErr.Error()})
|
|
return
|
|
}
|
|
|
|
if item.Image != "" && strings.HasPrefix(item.Image, "/uploads/") {
|
|
_ = os.Remove("." + item.Image)
|
|
}
|
|
|
|
saved, saveErr := utils.SaveOptimizedImage(file, "./uploads/posts", id, &utils.ImageOptions{
|
|
Width: config.AppConfig.PostImageWidth,
|
|
Height: config.AppConfig.PostImageHeight,
|
|
Quality: float32(config.AppConfig.PostImageQuality),
|
|
Format: config.AppConfig.PostImageFormat,
|
|
Mode: config.AppConfig.PostImageMode,
|
|
})
|
|
if saveErr != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save image: " + saveErr.Error()})
|
|
return
|
|
}
|
|
imagePtr = &saved
|
|
}
|
|
|
|
item, err := h.postService.UpdatePost(
|
|
id,
|
|
titlePtr,
|
|
contentPtr,
|
|
keywordsPtr,
|
|
imagePtr,
|
|
videoPtr,
|
|
categoryIDsPtr,
|
|
tagIDsPtr,
|
|
parentIDPtr,
|
|
parentIDSet,
|
|
slugPtr,
|
|
isActivePtr,
|
|
isFrontPtr,
|
|
)
|
|
if err != nil {
|
|
status := http.StatusInternalServerError
|
|
switch err.Error() {
|
|
case "post not found":
|
|
status = http.StatusNotFound
|
|
case "slug cannot be empty", "slug already exists", "one or more categories not found", "one or more tags not found":
|
|
status = http.StatusBadRequest
|
|
}
|
|
c.JSON(status, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, item)
|
|
}
|
|
|
|
// DeletePost godoc
|
|
// @Summary Delete a post (Admin)
|
|
// @Description Delete a post by ID
|
|
// @Tags admin
|
|
// @Security ApiKeyAuth
|
|
// @Param id path string true "Post ID"
|
|
// @Success 200 {object} map[string]string
|
|
// @Failure 404 {object} map[string]string
|
|
// @Router /admin/posts/{id} [delete]
|
|
// DeletePost deletes a post by ID.
|
|
func (h *PostHandler) DeletePost(c *gin.Context) {
|
|
id := c.Param("id")
|
|
if err := h.postService.DeletePost(id); err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"message": "Post deleted successfully"})
|
|
}
|
|
|
|
func parseIDList(c *gin.Context, key string) []string {
|
|
ids := c.PostFormArray(key)
|
|
if len(ids) == 0 {
|
|
raw := strings.TrimSpace(c.PostForm(key))
|
|
if raw == "" {
|
|
return ids
|
|
}
|
|
parts := strings.Split(raw, ",")
|
|
for _, part := range parts {
|
|
trimmed := strings.TrimSpace(part)
|
|
if trimmed != "" {
|
|
ids = append(ids, trimmed)
|
|
}
|
|
}
|
|
}
|
|
return ids
|
|
}
|
|
|
|
func hasIDList(c *gin.Context, key string) bool {
|
|
if len(c.PostFormArray(key)) > 0 {
|
|
return true
|
|
}
|
|
if raw := strings.TrimSpace(c.PostForm(key)); raw != "" {
|
|
return true
|
|
}
|
|
return false
|
|
}
|