first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:35:24 +03:00
commit bbbf76b184
592 changed files with 246870 additions and 0 deletions

View 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
}