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 }