first commit
This commit is contained in:
461
api/handlers/post_handler.go
Normal file
461
api/handlers/post_handler.go
Normal file
@@ -0,0 +1,461 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user