Files
goGin/app/controllers/BlogContraller.go
Beyhan Oğur 2a5b661443 first commit
2026-04-26 21:46:42 +03:00

2290 lines
70 KiB
Go

package controllers
import (
"errors"
"fmt"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
database "goGin/app/database/config"
"goGin/app/database/models"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// Post payloads
type PostPayload struct {
Title string `json:"title" binding:"required" form:"title"`
Slug string `json:"slug" form:"slug"`
Images string `json:"images" form:"images"`
Content string `json:"content" form:"content"`
CategoryIDs []uint `json:"category_ids" form:"category_ids"`
TagNames []string `json:"tag_names" form:"tag_names"`
}
// Post CRUD
// CreatePost godoc
// @Summary Create a post
// @Description Create a new blog post (supports multipart/form-data with image upload)
// @Tags posts
// @Security BearerAuth
// @Accept multipart/form-data
// @Produce json
// @Param title formData string true "Title"
// @Param slug formData string false "Slug"
// @Param content formData string false "Content"
// @Param category_ids formData []int false "Category IDs (repeatable)"
// @Param tag_names formData []string false "Tag names (repeatable)"
// @Param images formData file false "Image files (use 'images' or 'image' fields)"
// @Param width formData int false "Image width"
// @Param height formData int false "Image height"
// @Param quality formData int false "Image quality"
// @Param format formData string false "Image format"
// @Success 201 {object} controllers.PostResponse
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/v1/admin/posts [post]
func CreatePost(c *gin.Context) {
if database.DB == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"})
return
}
// Determine whether request is multipart/form-data reliably.
contentType := c.GetHeader("Content-Type")
isMultipart := false
if contentType != "" {
if strings.Contains(contentType, "multipart/form-data") {
isMultipart = true
}
}
// Also try parsing MultipartForm to be robust if header is missing/varies
if !isMultipart {
if _, err := c.MultipartForm(); err == nil {
isMultipart = true
}
}
var payload PostPayload
var width, height, quality int
var format string
var imagePaths []string
if isMultipart {
// parse basic fields from form
payload.Title = c.PostForm("title")
if payload.Title == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "title is required"})
return
}
payload.Slug = c.PostForm("slug")
payload.Content = c.PostForm("content")
// parse repeated form fields
catStrs := c.PostFormArray("category_ids")
for _, s := range catStrs {
if s == "" {
continue
}
if id, err := strconv.Atoi(s); err == nil && id > 0 {
payload.CategoryIDs = append(payload.CategoryIDs, uint(id))
}
}
payload.TagNames = c.PostFormArray("tag_names")
// image metadata
if v := c.PostForm("width"); v != "" {
if wi, err := strconv.Atoi(v); err == nil {
width = wi
}
}
if v := c.PostForm("height"); v != "" {
if hi, err := strconv.Atoi(v); err == nil {
height = hi
}
}
if v := c.PostForm("quality"); v != "" {
if qi, err := strconv.Atoi(v); err == nil {
quality = qi
}
}
format = c.PostForm("format")
// handle file uploads (support multiple files under 'images' or single 'image')
if form, err := c.MultipartForm(); err == nil && form != nil {
files := form.File["images"]
if len(files) == 0 {
files = form.File["image"]
}
if len(files) > 0 {
uploadDir := filepath.Join("uploads", "posts")
if err := os.MkdirAll(uploadDir, os.ModePerm); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create upload dir"})
return
}
for i, file := range files {
ext := filepath.Ext(file.Filename)
newName := fmt.Sprintf("post-%d-%d%s", time.Now().UnixNano(), i, ext)
destination := filepath.Join(uploadDir, newName)
if err := c.SaveUploadedFile(file, destination); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
imagePaths = append(imagePaths, "/uploads/posts/"+newName)
}
}
}
// if client provided image paths as form string
if imgStr := c.PostForm("images"); imgStr != "" && len(imagePaths) == 0 {
// accept comma separated paths
for _, p := range strings.Split(imgStr, ",") {
p = strings.TrimSpace(p)
if p != "" {
imagePaths = append(imagePaths, p)
}
}
}
} else {
// JSON path
if err := c.ShouldBindJSON(&payload); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// If JSON includes width/height/quality/format fields, map them
// Try reading them from payload.Images if encoded or rely on frontend sending them in payload (not implemented here)
}
post := models.Post{
Title: payload.Title,
Slug: payload.Slug,
Images: strings.Join(imagePaths, ","),
Content: payload.Content,
Width: width,
Height: height,
Quality: quality,
Format: format,
}
// Transaction and associations same as before
tx := database.DB.Begin()
if tx.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to start transaction"})
return
}
if err := tx.Create(&post).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Attach categories if provided
if len(payload.CategoryIDs) > 0 {
var cats []models.Category
if err := tx.Where("id IN ?", payload.CategoryIDs).Find(&cats).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if err := tx.Model(&post).Association("Categories").Replace(&cats); err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
}
// Attach tags (create if not exists)
if len(payload.TagNames) > 0 {
var tags []models.Tag
for _, name := range payload.TagNames {
if name == "" {
continue
}
var tag models.Tag
if err := tx.Where("name = ?", name).First(&tag).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
tag = models.Tag{Name: name}
if err := tx.Create(&tag).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
} else {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
}
tags = append(tags, tag)
}
if err := tx.Model(&post).Association("Tags").Replace(&tags); err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
}
if err := tx.Commit().Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to commit"})
return
}
// reload with associations
if err := database.DB.Preload("Categories").Preload("Tags").First(&post, post.ID).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"data": post})
}
// GetPost godoc
// @Summary Get a post by slug
// @Description Return a single post found by slug
// @Tags posts
// @Produce json
// @Param slug path string true "Post slug"
// @Success 200 {object} controllers.PostResponse
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /api/v1/posts/{slug} [get]
func GetPost(c *gin.Context) {
if database.DB == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"})
return
}
// slug param (router uses :slug)
slug := c.Param("slug")
if slug == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid slug"})
return
}
var post models.Post
if err := database.DB.Preload("Categories").Preload("Tags").Where("slug = ?", slug).First(&post).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "post not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": post})
}
// ListPosts godoc
// @Summary List posts
// @Description List posts with pagination and optional filters
// @Tags posts
// @Produce json
// @Param page query int false "Page number"
// @Param per_page query int false "Items per page"
// @Param category_id query int false "Filter by category id"
// @Param tag_id query int false "Filter by tag id"
// @Param q query string false "Search query"
// @Success 200 {object} controllers.PostListResponse
// @Failure 500 {object} map[string]string
// @Router /api/v1/posts [get]
func ListPosts(c *gin.Context) {
if database.DB == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"})
return
}
pageStr := c.DefaultQuery("page", "1")
perPageStr := c.DefaultQuery("per_page", "20")
page, _ := strconv.Atoi(pageStr)
perPage, _ := strconv.Atoi(perPageStr)
if page < 1 {
page = 1
}
if perPage < 1 {
perPage = 20
}
if perPage > 100 {
perPage = 100
}
offset := (page - 1) * perPage
query := database.DB.Model(&models.Post{}).Preload("Categories").Preload("Tags")
// filters
if catStr := c.Query("category_id"); catStr != "" {
if catID, err := strconv.Atoi(catStr); err == nil && catID > 0 {
query = query.Joins("JOIN post_categories pc ON pc.post_id = posts.id").Where("pc.category_id = ?", catID)
}
}
if tagStr := c.Query("tag_id"); tagStr != "" {
if tagID, err := strconv.Atoi(tagStr); err == nil && tagID > 0 {
query = query.Joins("JOIN post_tags pt ON pt.post_id = posts.id").Where("pt.tag_id = ?", tagID)
}
}
if q := c.Query("q"); q != "" {
like := "%" + q + "%"
query = query.Where("title LIKE ? OR content LIKE ?", like, like)
}
var total int64
if err := query.Distinct("posts.id").Count(&total).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
var posts []models.Post
if err := query.Order("created_at desc").Limit(perPage).Offset(offset).Find(&posts).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"items": posts, "total": total, "page": page, "per_page": perPage})
}
// Admin: List posts (supports soft-delete filter)
// AdminListPosts godoc
// @Summary Admin: List posts (supports soft-delete filter)
// @Description Admin listing of posts. Use ?soft=only to list only deleted, ?soft=with to include deleted, omit to exclude deleted.
// @Tags posts
// @Security BearerAuth
// @Produce json
// @Param page query int false "Page number"
// @Param per_page query int false "Items per page"
// @Param category_id query int false "Filter by category id"
// @Param tag_id query int false "Filter by tag id"
// @Param q query string false "Search query"
// @Param soft query string false "Soft delete filter: only|with"
// @Success 200 {object} controllers.PostListResponse
// @Failure 500 {object} map[string]string
// @Router /api/v1/admin/posts [get]
func AdminListPosts(c *gin.Context) {
if database.DB == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"})
return
}
pageStr := c.DefaultQuery("page", "1")
perPageStr := c.DefaultQuery("per_page", "20")
page, _ := strconv.Atoi(pageStr)
perPage, _ := strconv.Atoi(perPageStr)
if page < 1 {
page = 1
}
if perPage < 1 {
perPage = 20
}
if perPage > 200 {
perPage = 200
}
offset := (page - 1) * perPage
soft := c.Query("soft")
var query *gorm.DB
if soft == "only" {
query = database.DB.Unscoped().Model(&models.Post{}).Where("deleted_at IS NOT NULL")
} else if soft == "with" {
query = database.DB.Unscoped().Model(&models.Post{})
} else {
query = database.DB.Model(&models.Post{})
}
query = query.Preload("Categories").Preload("Tags")
if catStr := c.Query("category_id"); catStr != "" {
if catID, err := strconv.Atoi(catStr); err == nil && catID > 0 {
query = query.Joins("JOIN post_categories pc ON pc.post_id = posts.id").Where("pc.category_id = ?", catID)
}
}
if tagStr := c.Query("tag_id"); tagStr != "" {
if tagID, err := strconv.Atoi(tagStr); err == nil && tagID > 0 {
query = query.Joins("JOIN post_tags pt ON pt.post_id = posts.id").Where("pt.tag_id = ?", tagID)
}
}
if q := c.Query("q"); q != "" {
like := "%" + q + "%"
query = query.Where("title LIKE ? OR content LIKE ?", like, like)
}
var total int64
if err := query.Distinct("posts.id").Count(&total).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
var posts []models.Post
if err := query.Order("created_at desc").Limit(perPage).Offset(offset).Find(&posts).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"items": posts, "total": total, "page": page, "per_page": perPage})
}
// AdminGetPost godoc
// @Summary Admin: Get a post by id
// @Description Return a single post by id with categories and tags
// @Tags posts
// @Security BearerAuth
// @Produce json
// @Param id path int true "Post ID"
// @Success 200 {object} controllers.PostResponse
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/v1/admin/posts/{id} [get]
func AdminGetPost(c *gin.Context) {
if database.DB == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"})
return
}
idStr := c.Param("id")
id, err := strconv.Atoi(idStr)
if err != nil || id < 1 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
var post models.Post
if err := database.DB.Unscoped().Preload("Categories").Preload("Tags").First(&post, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "post not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": post})
}
// UpdatePost godoc
// @Summary Update a post
// @Description Update an existing blog post (supports multipart/form-data with image upload)
// @Tags posts
// @Security BearerAuth
// @Accept multipart/form-data
// @Produce json
// @Param id path int true "Post ID"
// @Param title formData string false "Title"
// @Param slug formData string false "Slug"
// @Param content formData string false "Content"
// @Param category_ids formData []int false "Category IDs (repeatable)"
// @Param tag_names formData []string false "Tag names (repeatable)"
// @Param images formData file false "Image files (use 'images' or 'image' fields)"
// @Param width formData int false "Image width"
// @Param height formData int false "Image height"
// @Param quality formData int false "Image quality"
// @Param format formData string false "Image format"
// @Success 200 {object} controllers.PostResponse
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/v1/admin/posts/{id} [put]
func UpdatePost(c *gin.Context) {
if database.DB == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"})
return
}
// id param (router uses :id)
idStr := c.Param("id")
id, err := strconv.Atoi(idStr)
if err != nil || id < 1 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
var post models.Post
if err := database.DB.Where("id = ?", id).First(&post).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "post not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Determine multipart
contentType := c.GetHeader("Content-Type")
isMultipart := false
if contentType != "" {
if strings.Contains(contentType, "multipart/form-data") {
isMultipart = true
}
}
if !isMultipart {
if _, err := c.MultipartForm(); err == nil {
isMultipart = true
}
}
var payload PostPayload
var width, height, quality int
var format string
var imagePaths []string
if isMultipart {
// read optional fields and update only when provided
if v := c.PostForm("title"); v != "" {
post.Title = v
}
if v := c.PostForm("slug"); v != "" {
post.Slug = v
}
if v := c.PostForm("content"); v != "" {
post.Content = v
}
// categories
catStrs := c.PostFormArray("category_ids")
if len(catStrs) > 0 {
var catIDs []uint
for _, s := range catStrs {
if s == "" {
continue
}
if idn, err := strconv.Atoi(s); err == nil && idn > 0 {
catIDs = append(catIDs, uint(idn))
}
}
payload.CategoryIDs = catIDs
}
// tags
tagArr := c.PostFormArray("tag_names")
if len(tagArr) > 0 {
payload.TagNames = tagArr
}
// image metadata
if v := c.PostForm("width"); v != "" {
if wi, err := strconv.Atoi(v); err == nil {
width = wi
}
}
if v := c.PostForm("height"); v != "" {
if hi, err := strconv.Atoi(v); err == nil {
height = hi
}
}
if v := c.PostForm("quality"); v != "" {
if qi, err := strconv.Atoi(v); err == nil {
quality = qi
}
}
if v := c.PostForm("format"); v != "" {
format = v
}
// handle file uploads
if form, err := c.MultipartForm(); err == nil && form != nil {
files := form.File["images"]
if len(files) == 0 {
files = form.File["image"]
}
if len(files) > 0 {
uploadDir := filepath.Join("uploads", "posts")
if err := os.MkdirAll(uploadDir, os.ModePerm); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create upload dir"})
return
}
for i, file := range files {
ext := filepath.Ext(file.Filename)
newName := fmt.Sprintf("post-%d-%d%s", time.Now().UnixNano(), i, ext)
destination := filepath.Join(uploadDir, newName)
if err := c.SaveUploadedFile(file, destination); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
imagePaths = append(imagePaths, "/uploads/posts/"+newName)
}
}
}
// if form provided images as string
if imgStr := c.PostForm("images"); imgStr != "" && len(imagePaths) == 0 {
for _, p := range strings.Split(imgStr, ",") {
p = strings.TrimSpace(p)
if p != "" {
imagePaths = append(imagePaths, p)
}
}
}
// apply imagePaths if any
if len(imagePaths) > 0 {
post.Images = strings.Join(imagePaths, ",")
}
// apply image metadata if provided
if width != 0 {
post.Width = width
}
if height != 0 {
post.Height = height
}
if quality != 0 {
post.Quality = quality
}
if format != "" {
post.Format = format
}
} else {
// JSON path - keep previous behavior but allow partial updates
if err := c.ShouldBindJSON(&payload); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if payload.Title != "" {
post.Title = payload.Title
}
if payload.Slug != "" {
post.Slug = payload.Slug
}
if payload.Content != "" {
post.Content = payload.Content
}
if payload.Images != "" {
post.Images = payload.Images
}
if len(payload.CategoryIDs) > 0 {
/* handled below via payload */
}
if len(payload.TagNames) > 0 {
/* handled below via payload */
}
}
// Transaction
tx := database.DB.Begin()
if tx.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to start transaction"})
return
}
if err := tx.Save(&post).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Update categories
if len(payload.CategoryIDs) > 0 {
// filter out invalid/zero ids
var filtered []uint
for _, cid := range payload.CategoryIDs {
if cid > 0 {
filtered = append(filtered, cid)
}
}
if len(filtered) > 0 {
var cats []models.Category
if err := tx.Where("id IN ?", filtered).Find(&cats).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if err := tx.Model(&post).Association("Categories").Replace(&cats); err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
} else {
// filtered list empty -> do nothing (avoid clearing categories when client sends [0])
}
} else {
// If no categories provided in payload and request was multipart, do nothing (keep existing)
}
// Update tags
if len(payload.TagNames) > 0 {
var tags []models.Tag
for _, name := range payload.TagNames {
if name == "" {
continue
}
var tag models.Tag
if err := tx.Where("name = ?", name).First(&tag).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
tag = models.Tag{Name: name}
if err := tx.Create(&tag).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
} else {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
}
tags = append(tags, tag)
}
if err := tx.Model(&post).Association("Tags").Replace(&tags); err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
}
if err := tx.Commit().Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to commit"})
return
}
// reload with associations
if err := database.DB.Preload("Categories").Preload("Tags").First(&post, post.ID).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": post})
}
// DeletePost godoc
// @Summary Delete a post
// @Description Delete a blog post by ID
// @Tags posts
// @Security BearerAuth
// @Produce json
// @Param id path int true "Post ID"
// @Success 204 {object} nil
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/v1/admin/posts/{id} [delete]
func DeletePost(c *gin.Context) {
if database.DB == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"})
return
}
// id param (router uses :id)
idStr := c.Param("id")
id, err := strconv.Atoi(idStr)
if err != nil || id < 1 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
var post models.Post
if err := database.DB.Where("id = ?", id).First(&post).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "post not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Delete the post (soft delete if model has DeletedAt)
if err := database.DB.Delete(&post).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Return a helpful JSON response instead of empty body
c.JSON(http.StatusOK, gin.H{
"message": "post deleted successfully",
"id": post.ID,
})
}
// Category CRUD
func GetCategory(c *gin.Context) {
if database.DB == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"})
return
}
// slug param
slug := c.Param("slug")
if slug == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid slug"})
return
}
var cat models.Category
if err := database.DB.Preload("Posts").Where("slug = ?", slug).First(&cat).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "category not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": cat})
}
// Payloads for other models
type CategoryPayload struct {
Title string `json:"title" binding:"required"`
Slug string `json:"slug"`
Description string `json:"description"`
ParentID *uint `json:"parent_id"`
}
type TagPayload struct {
Name string `json:"name" binding:"required"`
}
type CommentPayload struct {
UserID uint `json:"user_id" binding:"required"`
PostID uint `json:"post_id" binding:"required"`
Body string `json:"body" binding:"required"`
}
type CategoryViewPayload struct {
CategoryID uint `json:"category_id" binding:"required"`
IPAddress string `json:"ip_address"`
}
// Admin: CreateCategory
// CreateCategory godoc
// @Summary Create a category
// @Description Create a new category (admin)
// @Tags categories
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param category body CategoryPayload true "Category payload"
// @Success 201 {object} controllers.CategorySimple
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/v1/admin/categories [post]
func CreateCategory(c *gin.Context) {
if database.DB == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"})
return
}
var payload CategoryPayload
if err := c.ShouldBindJSON(&payload); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
cat := models.Category{Title: payload.Title, Slug: payload.Slug, Description: payload.Description, ParentID: payload.ParentID}
if err := database.DB.Create(&cat).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
resp := CategorySimple{ID: cat.ID, Title: cat.Title, Slug: cat.Slug, ParentID: cat.ParentID}
c.JSON(http.StatusCreated, gin.H{"data": resp})
}
// Admin: UpdateCategory
// UpdateCategory godoc
// @Summary Update a category
// @Description Update an existing category (admin)
// @Tags categories
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path int true "Category ID"
// @Param category body CategoryPayload true "Category payload"
// @Success 200 {object} controllers.CategorySimple
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/v1/admin/categories/{id} [put]
func UpdateCategory(c *gin.Context) {
if database.DB == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"})
return
}
idStr := c.Param("id")
id, err := strconv.Atoi(idStr)
if err != nil || id < 1 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
var payload CategoryPayload
if err := c.ShouldBindJSON(&payload); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var cat models.Category
if err := database.DB.First(&cat, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "category not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
cat.Title = payload.Title
cat.Slug = payload.Slug
cat.Description = payload.Description
cat.ParentID = payload.ParentID
if err := database.DB.Save(&cat).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
resp := CategorySimple{ID: cat.ID, Title: cat.Title, Slug: cat.Slug, ParentID: cat.ParentID}
c.JSON(http.StatusOK, gin.H{"data": resp})
}
// Admin: DeleteCategory
// DeleteCategory godoc
// @Summary Delete a category
// @Description Soft-delete a category (admin)
// @Tags categories
// @Security BearerAuth
// @Produce json
// @Param id path int true "Category ID"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/v1/admin/categories/{id} [delete]
func DeleteCategory(c *gin.Context) {
if database.DB == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"})
return
}
idStr := c.Param("id")
id, err := strconv.Atoi(idStr)
if err != nil || id < 1 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
var cat models.Category
if err := database.DB.First(&cat, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "category not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if err := database.DB.Delete(&cat).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "category deleted successfully",
"id": cat.ID,
})
}
// Public: GetTag
// GetTag godoc
// @Summary Get a tag by id
// @Description Return a single tag by id
// @Tags tags
// @Produce json
// @Param id path int true "Tag ID"
// @Success 200 {object} controllers.TagSimple
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /api/v1/tags/{id} [get]
func GetTag(c *gin.Context) {
if database.DB == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"})
return
}
idStr := c.Param("id")
id, err := strconv.Atoi(idStr)
if err != nil || id < 1 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
var t models.Tag
if err := database.DB.First(&t, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "tag not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": TagSimple{ID: t.ID, Name: t.Name}})
}
// Admin: CreateTag
// CreateTag godoc
// @Summary Create a tag
// @Description Create a new tag (admin)
// @Tags tags
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param tag body TagPayload true "Tag payload"
// @Success 201 {object} controllers.TagSimple
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/v1/admin/tags [post]
func CreateTag(c *gin.Context) {
if database.DB == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"})
return
}
var payload TagPayload
if err := c.ShouldBindJSON(&payload); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
tag := models.Tag{Name: payload.Name}
if err := database.DB.Create(&tag).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"data": TagSimple{ID: tag.ID, Name: tag.Name}})
}
// Admin: UpdateTag
// UpdateTag godoc
// @Summary Update a tag
// @Description Update an existing tag (admin)
// @Tags tags
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path int true "Tag ID"
// @Param tag body TagPayload true "Tag payload"
// @Success 200 {object} controllers.TagSimple
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/v1/admin/tags/{id} [put]
func UpdateTag(c *gin.Context) {
if database.DB == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"})
return
}
idStr := c.Param("id")
id, err := strconv.Atoi(idStr)
if err != nil || id < 1 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
var payload TagPayload
if err := c.ShouldBindJSON(&payload); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var tag models.Tag
if err := database.DB.First(&tag, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "tag not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
tag.Name = payload.Name
if err := database.DB.Save(&tag).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": TagSimple{ID: tag.ID, Name: tag.Name}})
}
// Admin: DeleteTag
// DeleteTag godoc
// @Summary Delete a tag
// @Description Soft-delete a tag (admin)
// @Tags tags
// @Security BearerAuth
// @Produce json
// @Param id path int true "Tag ID"
// @Success 204 {object} nil
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/v1/admin/tags/{id} [delete]
func DeleteTag(c *gin.Context) {
if database.DB == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"})
return
}
idStr := c.Param("id")
id, err := strconv.Atoi(idStr)
if err != nil || id < 1 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
var tag models.Tag
if err := database.DB.First(&tag, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "tag not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if err := database.DB.Delete(&tag).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.Status(http.StatusNoContent)
}
// RestoreTag godoc
// @Summary Restore a soft-deleted tag
// @Description Restore a tag that has been soft-deleted (admin)
// @Tags tags
// @Security BearerAuth
// @Produce json
// @Param id path int true "Tag ID"
// @Success 200 {object} controllers.TagSimple
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/v1/admin/tags/{id}/restore [post]
func RestoreTag(c *gin.Context) {
if database.DB == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"})
return
}
idStr := c.Param("id")
id, err := strconv.Atoi(idStr)
if err != nil || id < 1 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
var tag models.Tag
if err := database.DB.Unscoped().First(&tag, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "tag not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if tag.DeletedAt.Valid {
if err := database.DB.Unscoped().Model(&tag).Update("deleted_at", nil).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
}
c.JSON(http.StatusOK, gin.H{"data": TagSimple{ID: tag.ID, Name: tag.Name}})
}
// Public: GetComment
// GetComment godoc
// @Summary Get a comment by id
// @Description Return a single comment by id
// @Tags comments
// @Produce json
// @Param id path int true "Comment ID"
// @Success 200 {object} controllers.CommentSimple
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /api/v1/comments/{id} [get]
func GetComment(c *gin.Context) {
if database.DB == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"})
return
}
idStr := c.Param("id")
id, err := strconv.Atoi(idStr)
if err != nil || id < 1 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
var cm models.Comment
if err := database.DB.First(&cm, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "comment not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": CommentSimple{ID: cm.ID, UserID: cm.UserID, PostID: cm.PostID, Body: cm.Body, Created: cm.CreatedAt}})
}
// Admin: CreateComment
// CreateComment godoc
// @Summary Create a comment (admin)
// @Description Create a comment as admin
// @Tags comments
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param comment body CommentPayload true "Comment payload"
// @Success 201 {object} controllers.CommentSimple
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/v1/admin/comments [post]
func CreateComment(c *gin.Context) {
if database.DB == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"})
return
}
var payload CommentPayload
if err := c.ShouldBindJSON(&payload); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
cm := models.Comment{UserID: payload.UserID, PostID: payload.PostID, Body: payload.Body}
if err := database.DB.Create(&cm).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"data": CommentSimple{ID: cm.ID, UserID: cm.UserID, PostID: cm.PostID, Body: cm.Body, Created: cm.CreatedAt}})
}
// Admin: UpdateComment
// UpdateComment godoc
// @Summary Update a comment (admin)
// @Description Update a comment as admin
// @Tags comments
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path int true "Comment ID"
// @Param comment body CommentPayload true "Comment payload"
// @Success 200 {object} controllers.CommentSimple
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/v1/admin/comments/{id} [put]
func UpdateComment(c *gin.Context) {
if database.DB == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"})
return
}
idStr := c.Param("id")
id, err := strconv.Atoi(idStr)
if err != nil || id < 1 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
var payload CommentPayload
if err := c.ShouldBindJSON(&payload); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var cm models.Comment
if err := database.DB.First(&cm, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "comment not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
cm.Body = payload.Body
if err := database.DB.Save(&cm).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": CommentSimple{ID: cm.ID, UserID: cm.UserID, PostID: cm.PostID, Body: cm.Body, Created: cm.CreatedAt}})
}
// Admin: DeleteComment
// DeleteComment godoc
// @Summary Delete a comment (admin)
// @Description Soft-delete a comment as admin
// @Tags comments
// @Security BearerAuth
// @Produce json
// @Param id path int true "Comment ID"
// @Success 204 {object} nil
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/v1/admin/comments/{id} [delete]
func DeleteComment(c *gin.Context) {
if database.DB == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"})
return
}
idStr := c.Param("id")
id, err := strconv.Atoi(idStr)
if err != nil || id < 1 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
var cm models.Comment
if err := database.DB.First(&cm, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "comment not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if err := database.DB.Delete(&cm).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.Status(http.StatusNoContent)
}
// Public: GetCategoryView
// GetCategoryView godoc
// @Summary Get a category view by id
// @Description Return a single category view by id
// @Tags categoryviews
// @Produce json
// @Param id path int true "CategoryView ID"
// @Success 200 {object} controllers.CategoryViewSimple
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /api/v1/categoryviews/{id} [get]
func GetCategoryView(c *gin.Context) {
if database.DB == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"})
return
}
idStr := c.Param("id")
id, err := strconv.Atoi(idStr)
if err != nil || id < 1 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
var cv models.CategoryView
if err := database.DB.First(&cv, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "category view not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": CategoryViewSimple{ID: cv.ID, CategoryID: cv.CategoryID, IPAddress: cv.IPAddress, Created: cv.CreatedAt}})
}
// Admin: CreateCategoryView
// CreateCategoryView godoc
// @Summary Create a category view (admin)
// @Description Create a category view as admin
// @Tags categoryviews
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param view body CategoryViewPayload true "CategoryView payload"
// @Success 201 {object} controllers.CategoryViewSimple
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/v1/admin/categoryviews [post]
func CreateCategoryView(c *gin.Context) {
if database.DB == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"})
return
}
var payload CategoryViewPayload
if err := c.ShouldBindJSON(&payload); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
cv := models.CategoryView{CategoryID: payload.CategoryID, IPAddress: payload.IPAddress}
if err := database.DB.Create(&cv).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"data": CategoryViewSimple{ID: cv.ID, CategoryID: cv.CategoryID, IPAddress: cv.IPAddress, Created: cv.CreatedAt}})
}
// İlişkili işlemler
func FilterPostsByTag(c *gin.Context) { _ = c }
// ListDeletedPosts godoc
// @Summary List soft-deleted posts
// @Description List posts that have been soft-deleted with pagination
// @Tags posts
// @Security BearerAuth
// @Produce json
// @Param page query int false "Page number"
// @Param per_page query int false "Items per page"
// @Success 200 {object} controllers.PostListResponse
// @Failure 500 {object} map[string]string
// @Router /api/v1/admin/posts/deleted [get]
func ListDeletedPosts(c *gin.Context) {
if database.DB == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"})
return
}
pageStr := c.DefaultQuery("page", "1")
perPageStr := c.DefaultQuery("per_page", "20")
page, _ := strconv.Atoi(pageStr)
perPage, _ := strconv.Atoi(perPageStr)
if page < 1 {
page = 1
}
if perPage < 1 {
perPage = 20
}
if perPage > 200 {
perPage = 200
}
offset := (page - 1) * perPage
query := database.DB.Unscoped().Model(&models.Post{}).Where("deleted_at IS NOT NULL").Preload("Categories").Preload("Tags")
var total int64
if err := query.Distinct("posts.id").Count(&total).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
var posts []models.Post
if err := query.Order("deleted_at desc").Limit(perPage).Offset(offset).Find(&posts).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
var respItems []PostResponse
for _, p := range posts {
var cats []CategorySimple
for _, cc := range p.Categories {
cats = append(cats, CategorySimple{ID: cc.ID, Title: cc.Title, Slug: cc.Slug, ParentID: cc.ParentID})
}
var tags []TagSimple
for _, t := range p.Tags {
tags = append(tags, TagSimple{ID: t.ID, Name: t.Name})
}
respItems = append(respItems, PostResponse{
ID: p.ID,
Title: p.Title,
Slug: p.Slug,
Images: p.Images,
Content: p.Content,
Categories: cats,
Tags: tags,
CreatedAt: p.CreatedAt,
UpdatedAt: p.UpdatedAt,
})
}
c.JSON(http.StatusOK, gin.H{"items": respItems, "total": total, "page": page, "per_page": perPage})
}
// RestorePost godoc
// @Summary Restore a soft-deleted post
// @Description Restore a post and its related comments (if soft-deleted)
// @Tags posts
// @Security BearerAuth
// @Produce json
// @Param id path int true "Post ID"
// @Success 200 {object} controllers.PostResponse
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/v1/admin/posts/{id}/restore [post]
func RestorePost(c *gin.Context) {
if database.DB == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"})
return
}
idStr := c.Param("id")
id, err := strconv.Atoi(idStr)
if err != nil || id <= 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
tx := database.DB.Begin()
if tx.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to start transaction"})
return
}
var post models.Post
if err := tx.Unscoped().Preload("Categories").Preload("Tags").First(&post, id).Error; err != nil {
tx.Rollback()
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "post not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if post.DeletedAt.Time.IsZero() {
tx.Rollback()
c.JSON(http.StatusBadRequest, gin.H{"error": "post is not deleted"})
return
}
// restore post
if err := tx.Unscoped().Model(&post).Update("deleted_at", nil).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// restore related comments (if any)
if err := tx.Unscoped().Model(&models.Comment{}).Where("post_id = ? AND deleted_at IS NOT NULL", post.ID).Update("deleted_at", nil).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if err := tx.Commit().Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to commit"})
return
}
if err := database.DB.Preload("Categories").Preload("Tags").First(&post, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
var cats []CategorySimple
for _, cc := range post.Categories {
cats = append(cats, CategorySimple{ID: cc.ID, Title: cc.Title, Slug: cc.Slug, ParentID: cc.ParentID})
}
var tags []TagSimple
for _, t := range post.Tags {
tags = append(tags, TagSimple{ID: t.ID, Name: t.Name})
}
resp := PostResponse{ID: post.ID, Title: post.Title, Slug: post.Slug, Images: post.Images, Content: post.Content, Categories: cats, Tags: tags, CreatedAt: post.CreatedAt, UpdatedAt: post.UpdatedAt}
c.JSON(http.StatusOK, gin.H{"data": resp})
}
// ListDeletedCategories godoc
// @Summary List soft-deleted categories
// @Description List categories that have been soft-deleted with pagination
// @Tags categories
// @Security BearerAuth
// @Produce json
// @Param page query int false "Page number"
// @Param per_page query int false "Items per page"
// @Success 200 {object} map[string]interface{}
// @Failure 500 {object} map[string]string
// @Router /api/v1/admin/categories/deleted [get]
func ListDeletedCategories(c *gin.Context) {
if database.DB == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"})
return
}
pageStr := c.DefaultQuery("page", "1")
perPageStr := c.DefaultQuery("per_page", "20")
page, _ := strconv.Atoi(pageStr)
perPage, _ := strconv.Atoi(perPageStr)
if page < 1 {
page = 1
}
if perPage < 1 {
perPage = 20
}
if perPage > 200 {
perPage = 200
}
offset := (page - 1) * perPage
query := database.DB.Unscoped().Model(&models.Category{}).Where("deleted_at IS NOT NULL")
var total int64
if err := query.Distinct("categories.id").Count(&total).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
var cats []models.Category
if err := query.Order("deleted_at desc").Limit(perPage).Offset(offset).Find(&cats).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
var resp []CategorySimple
for _, cc := range cats {
resp = append(resp, CategorySimple{ID: cc.ID, Title: cc.Title, Slug: cc.Slug, ParentID: cc.ParentID})
}
c.JSON(http.StatusOK, gin.H{"items": resp, "total": total, "page": page, "per_page": perPage})
}
// RestoreCategory godoc
// @Summary Restore a soft-deleted category
// @Description Restore a category and related posts/comments if soft-deleted
// @Tags categories
// @Security BearerAuth
// @Produce json
// @Param id path int true "Category ID"
// @Success 200 {object} controllers.CategorySimple
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/v1/admin/categories/{id}/restore [post]
func RestoreCategory(c *gin.Context) {
if database.DB == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"})
return
}
idStr := c.Param("id")
id, err := strconv.Atoi(idStr)
if err != nil || id <= 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
tx := database.DB.Begin()
if tx.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to start transaction"})
return
}
var cat models.Category
if err := tx.Unscoped().First(&cat, id).Error; err != nil {
tx.Rollback()
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "category not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if cat.DeletedAt.Time.IsZero() {
tx.Rollback()
c.JSON(http.StatusBadRequest, gin.H{"error": "category is not deleted"})
return
}
// restore category
if err := tx.Unscoped().Model(&cat).Update("deleted_at", nil).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// find post ids related to this category that are soft-deleted
var postIDs []uint
if err := tx.Table("posts").Select("posts.id").Joins("JOIN post_categories pc ON pc.post_id = posts.id").Where("pc.category_id = ? AND posts.deleted_at IS NOT NULL", cat.ID).Scan(&postIDs).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if len(postIDs) > 0 {
// restore posts
if err := tx.Unscoped().Model(&models.Post{}).Where("id IN ?", postIDs).Update("deleted_at", nil).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// restore comments for those posts
if err := tx.Unscoped().Model(&models.Comment{}).Where("post_id IN ? AND deleted_at IS NOT NULL", postIDs).Update("deleted_at", nil).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
}
if err := tx.Commit().Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to commit"})
return
}
resp := CategorySimple{ID: cat.ID, Title: cat.Title, Slug: cat.Slug, ParentID: cat.ParentID}
c.JSON(http.StatusOK, gin.H{"data": resp})
}
// ListCategories godoc
// @Summary List categories
// @Description List categories with pagination
// @Tags categories
// @Produce json
// @Param page query int false "Page number"
// @Param per_page query int false "Items per page"
// @Success 200 {object} map[string]interface{}
// @Failure 500 {object} map[string]string
// @Router /api/v1/categories [get]
func ListCategories(c *gin.Context) {
if database.DB == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"})
return
}
pageStr := c.DefaultQuery("page", "1")
perPageStr := c.DefaultQuery("per_page", "20")
page, _ := strconv.Atoi(pageStr)
perPage, _ := strconv.Atoi(perPageStr)
if page < 1 {
page = 1
}
if perPage < 1 {
perPage = 20
}
if perPage > 200 {
perPage = 200
}
offset := (page - 1) * perPage
var total int64
if err := database.DB.Model(&models.Category{}).Count(&total).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
var cats []models.Category
if err := database.DB.Order("created_at desc").Limit(perPage).Offset(offset).Find(&cats).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
var resp []CategorySimple
for _, cc := range cats {
resp = append(resp, CategorySimple{ID: cc.ID, Title: cc.Title, Slug: cc.Slug, ParentID: cc.ParentID})
}
c.JSON(http.StatusOK, gin.H{"items": resp, "total": total, "page": page, "per_page": perPage})
}
// AdminListCategories godoc
// @Summary Admin: List categories (supports soft-delete filter)
// @Description Admin listing of categories. Use ?soft=only to list only deleted, ?soft=with to include deleted, omit to exclude deleted.
// @Tags categories
// @Security BearerAuth
// @Produce json
// @Param page query int false "Page number"
// @Param per_page query int false "Items per page"
// @Param soft query string false "Soft delete filter: only|with"
// @Success 200 {object} map[string]interface{}
// @Failure 500 {object} map[string]string
// @Router /api/v1/admin/categories [get]
func AdminListCategories(c *gin.Context) {
if database.DB == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"})
return
}
pageStr := c.DefaultQuery("page", "1")
perPageStr := c.DefaultQuery("per_page", "20")
page, _ := strconv.Atoi(pageStr)
perPage, _ := strconv.Atoi(perPageStr)
if page < 1 {
page = 1
}
if perPage < 1 {
perPage = 20
}
if perPage > 200 {
perPage = 200
}
offset := (page - 1) * perPage
soft := c.Query("soft")
var query *gorm.DB
if soft == "only" {
query = database.DB.Unscoped().Model(&models.Category{}).Where("deleted_at IS NOT NULL")
} else if soft == "with" {
query = database.DB.Unscoped().Model(&models.Category{})
} else {
query = database.DB.Model(&models.Category{})
}
query = query.Table("categories")
var total int64
if err := query.Count(&total).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
var items []struct {
ID uint `gorm:"column:id"`
Title string `gorm:"column:title"`
Slug string `gorm:"column:slug"`
ParentID *uint `gorm:"column:parent_id"`
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at"`
}
if err := query.Select("id", "title", "slug", "parent_id", "deleted_at").Order("created_at desc").Limit(perPage).Offset(offset).Find(&items).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
var resp []AdminCategoryListItem
for _, v := range items {
item := AdminCategoryListItem{ID: v.ID, Title: v.Title, Slug: v.Slug, ParentID: v.ParentID}
if v.DeletedAt.Valid {
item.DeletedAt = &v.DeletedAt.Time
}
resp = append(resp, item)
}
c.JSON(http.StatusOK, gin.H{"items": resp, "total": total, "page": page, "per_page": perPage})
}
// ListTags godoc
// @Summary List tags
// @Description List tags with pagination
// @Tags tags
// @Produce json
// @Param page query int false "Page number"
// @Param per_page query int false "Items per page"
// @Success 200 {object} map[string]interface{}
// @Failure 500 {object} map[string]string
// @Router /api/v1/tags [get]
func ListTags(c *gin.Context) {
if database.DB == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"})
return
}
pageStr := c.DefaultQuery("page", "1")
perPageStr := c.DefaultQuery("per_page", "20")
page, _ := strconv.Atoi(pageStr)
perPage, _ := strconv.Atoi(perPageStr)
if page < 1 {
page = 1
}
if perPage < 1 {
perPage = 20
}
if perPage > 200 {
perPage = 200
}
offset := (page - 1) * perPage
var total int64
if err := database.DB.Model(&models.Tag{}).Count(&total).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
var items []models.Tag
if err := database.DB.Order("created_at desc").Limit(perPage).Offset(offset).Find(&items).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
var resp []TagSimple
for _, v := range items {
resp = append(resp, TagSimple{ID: v.ID, Name: v.Name})
}
c.JSON(http.StatusOK, gin.H{"items": resp, "total": total, "page": page, "per_page": perPage})
}
// AdminListTags godoc
// @Summary Admin: List tags (supports soft-delete filter)
// @Description Admin listing of tags. Use ?soft=only to list only deleted, ?soft=with to include deleted, omit to exclude deleted.
// @Tags tags
// @Security BearerAuth
// @Produce json
// @Param page query int false "Page number"
// @Param per_page query int false "Items per page"
// @Param soft query string false "Soft delete filter: only|with"
// @Success 200 {object} map[string]interface{} "items: []AdminTagListItem (includes deleted_at when soft=only or soft=with)"
// @Failure 500 {object} map[string]string
// @Router /api/v1/admin/tags [get]
func AdminListTags(c *gin.Context) {
if database.DB == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"})
return
}
pageStr := c.DefaultQuery("page", "1")
perPageStr := c.DefaultQuery("per_page", "20")
page, _ := strconv.Atoi(pageStr)
perPage, _ := strconv.Atoi(perPageStr)
if page < 1 {
page = 1
}
if perPage < 1 {
perPage = 20
}
if perPage > 200 {
perPage = 200
}
offset := (page - 1) * perPage
soft := c.Query("soft")
var query *gorm.DB
if soft == "only" {
query = database.DB.Unscoped().Model(&models.Tag{}).Where("deleted_at IS NOT NULL")
} else if soft == "with" {
query = database.DB.Unscoped().Model(&models.Tag{})
} else {
query = database.DB.Model(&models.Tag{})
}
query = query.Table("tags")
var total int64
if err := query.Count(&total).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
var items []struct {
ID uint `gorm:"column:id"`
Name string `gorm:"column:name"`
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at"`
}
if err := query.Select("id", "name", "deleted_at").Order("created_at desc").Limit(perPage).Offset(offset).Find(&items).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
var resp []AdminTagListItem
for _, v := range items {
item := AdminTagListItem{ID: v.ID, Name: v.Name}
if v.DeletedAt.Valid {
item.DeletedAt = &v.DeletedAt.Time
}
resp = append(resp, item)
}
c.JSON(http.StatusOK, gin.H{"items": resp, "total": total, "page": page, "per_page": perPage})
}
// ListComments godoc
// @Summary List comments
// @Description List comments with pagination
// @Tags comments
// @Produce json
// @Param page query int false "Page number"
// @Param per_page query int false "Items per page"
// @Success 200 {object} map[string]interface{}
// @Failure 500 {object} map[string]string
// @Router /api/v1/comments [get]
func ListComments(c *gin.Context) {
if database.DB == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"})
return
}
pageStr := c.DefaultQuery("page", "1")
perPageStr := c.DefaultQuery("per_page", "20")
page, _ := strconv.Atoi(pageStr)
perPage, _ := strconv.Atoi(perPageStr)
if page < 1 {
page = 1
}
if perPage < 1 {
perPage = 20
}
if perPage > 200 {
perPage = 200
}
offset := (page - 1) * perPage
var total int64
if err := database.DB.Model(&models.Comment{}).Count(&total).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
var items []models.Comment
if err := database.DB.Order("created_at desc").Limit(perPage).Offset(offset).Find(&items).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
var resp []CommentSimple
for _, v := range items {
resp = append(resp, CommentSimple{ID: v.ID, UserID: v.UserID, PostID: v.PostID, Body: v.Body, Created: v.CreatedAt})
}
c.JSON(http.StatusOK, gin.H{"items": resp, "total": total, "page": page, "per_page": perPage})
}
// AdminListComments godoc
// @Summary Admin: List comments (supports soft-delete filter)
// @Description Admin listing of comments. Use ?soft=only to list only deleted, ?soft=with to include deleted, omit to exclude deleted.
// @Tags comments
// @Security BearerAuth
// @Produce json
// @Param page query int false "Page number"
// @Param per_page query int false "Items per page"
// @Param soft query string false "Soft delete filter: only|with"
// @Success 200 {object} map[string]interface{}
// @Failure 500 {object} map[string]string
// @Router /api/v1/admin/comments [get]
func AdminListComments(c *gin.Context) {
if database.DB == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"})
return
}
pageStr := c.DefaultQuery("page", "1")
perPageStr := c.DefaultQuery("per_page", "20")
page, _ := strconv.Atoi(pageStr)
perPage, _ := strconv.Atoi(perPageStr)
if page < 1 {
page = 1
}
if perPage < 1 {
perPage = 20
}
if perPage > 200 {
perPage = 200
}
offset := (page - 1) * perPage
soft := c.Query("soft")
var query *gorm.DB
if soft == "only" {
query = database.DB.Unscoped().Model(&models.Comment{}).Where("deleted_at IS NOT NULL")
} else if soft == "with" {
query = database.DB.Unscoped().Model(&models.Comment{})
} else {
query = database.DB.Model(&models.Comment{})
}
var total int64
if err := query.Count(&total).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
var items []models.Comment
if err := query.Order("created_at desc").Limit(perPage).Offset(offset).Find(&items).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
var resp []CommentSimple
for _, v := range items {
resp = append(resp, CommentSimple{ID: v.ID, UserID: v.UserID, PostID: v.PostID, Body: v.Body, Created: v.CreatedAt})
}
c.JSON(http.StatusOK, gin.H{"items": resp, "total": total, "page": page, "per_page": perPage})
}
// ListCategoryViews godoc
// @Summary List category views
// @Description List category views with pagination
// @Tags categoryviews
// @Produce json
// @Param page query int false "Page number"
// @Param per_page query int false "Items per page"
// @Success 200 {object} map[string]interface{}
// @Failure 500 {object} map[string]string
// @Router /api/v1/categoryviews [get]
func ListCategoryViews(c *gin.Context) {
if database.DB == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"})
return
}
pageStr := c.DefaultQuery("page", "1")
perPageStr := c.DefaultQuery("per_page", "20")
page, _ := strconv.Atoi(pageStr)
perPage, _ := strconv.Atoi(perPageStr)
if page < 1 {
page = 1
}
if perPage < 1 {
perPage = 20
}
if perPage > 200 {
perPage = 200
}
offset := (page - 1) * perPage
var total int64
if err := database.DB.Model(&models.CategoryView{}).Count(&total).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
var items []models.CategoryView
if err := database.DB.Order("created_at desc").Limit(perPage).Offset(offset).Find(&items).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
var resp []CategoryViewSimple
for _, v := range items {
resp = append(resp, CategoryViewSimple{ID: v.ID, CategoryID: v.CategoryID, IPAddress: v.IPAddress, Created: v.CreatedAt})
}
c.JSON(http.StatusOK, gin.H{"items": resp, "total": total, "page": page, "per_page": perPage})
}
// AdminListCategoryViews godoc
// @Summary Admin: List category views (supports soft-delete filter)
// @Description Admin listing of category views. Use ?soft=only to list only deleted, ?soft=with to include deleted, omit to exclude deleted.
// @Tags categoryviews
// @Security BearerAuth
// @Produce json
// @Param page query int false "Page number"
// @Param per_page query int false "Items per page"
// @Param soft query string false "Soft delete filter: only|with"
// @Success 200 {object} map[string]interface{}
// @Failure 500 {object} map[string]string
// @Router /api/v1/admin/categoryviews [get]
func AdminListCategoryViews(c *gin.Context) {
if database.DB == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"})
return
}
pageStr := c.DefaultQuery("page", "1")
perPageStr := c.DefaultQuery("per_page", "20")
page, _ := strconv.Atoi(pageStr)
perPage, _ := strconv.Atoi(perPageStr)
if page < 1 {
page = 1
}
if perPage < 1 {
perPage = 20
}
if perPage > 200 {
perPage = 200
}
offset := (page - 1) * perPage
soft := c.Query("soft")
var query *gorm.DB
if soft == "only" {
query = database.DB.Unscoped().Model(&models.CategoryView{}).Where("deleted_at IS NOT NULL")
} else if soft == "with" {
query = database.DB.Unscoped().Model(&models.CategoryView{})
} else {
query = database.DB.Model(&models.CategoryView{})
}
var total int64
if err := query.Count(&total).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
var items []models.CategoryView
if err := query.Order("created_at desc").Limit(perPage).Offset(offset).Find(&items).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
var resp []CategoryViewSimple
for _, v := range items {
resp = append(resp, CategoryViewSimple{ID: v.ID, CategoryID: v.CategoryID, IPAddress: v.IPAddress, Created: v.CreatedAt})
}
c.JSON(http.StatusOK, gin.H{"items": resp, "total": total, "page": page, "per_page": perPage})
}
// Admin: AddCommentToPost
// AddCommentToPost godoc
// @Summary Add a comment to a post (admin)
// @Description Add a comment to a specific post as admin
// @Tags posts
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path int true "Post ID"
// @Param comment body CommentPayload true "Comment payload"
// @Success 201 {object} controllers.CommentSimple
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/v1/admin/posts/{id}/comments [post]
func AddCommentToPost(c *gin.Context) {
if database.DB == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"})
return
}
idStr := c.Param("id")
id, err := strconv.Atoi(idStr)
if err != nil || id < 1 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid post id"})
return
}
var payload CommentPayload
if err := c.ShouldBindJSON(&payload); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var post models.Post
if err := database.DB.First(&post, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "post not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
cm := models.Comment{UserID: payload.UserID, PostID: uint(id), Body: payload.Body}
if err := database.DB.Create(&cm).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"data": CommentSimple{ID: cm.ID, UserID: cm.UserID, PostID: cm.PostID, Body: cm.Body, Created: cm.CreatedAt}})
}
// Admin: AddPostToCategory
// AddPostToCategory godoc
// @Summary Add a post to a category (admin)
// @Description Create a post and attach it to the given category
// @Tags categories
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path int true "Category ID"
// @Param post body PostPayload true "Post payload"
// @Success 201 {object} controllers.PostResponse
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/v1/admin/categories/{id}/posts [post]
func AddPostToCategory(c *gin.Context) {
if database.DB == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"})
return
}
catIDStr := c.Param("id")
catID, err := strconv.Atoi(catIDStr)
if err != nil || catID < 1 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid category id"})
return
}
var payload PostPayload
if err := c.ShouldBindJSON(&payload); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var cat models.Category
if err := database.DB.First(&cat, catID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "category not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
post := models.Post{Title: payload.Title, Slug: payload.Slug, Images: payload.Images, Content: payload.Content}
tx := database.DB.Begin()
if tx.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to start transaction"})
return
}
if err := tx.Create(&post).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if err := tx.Model(&post).Association("Categories").Append(&cat); err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if len(payload.CategoryIDs) > 0 {
var cats []models.Category
if err := tx.Where("id IN ?", payload.CategoryIDs).Find(&cats).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if err := tx.Model(&post).Association("Categories").Replace(&cats); err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
}
if len(payload.TagNames) > 0 {
var tags []models.Tag
for _, name := range payload.TagNames {
if name == "" {
continue
}
var tag models.Tag
if err := tx.Where("name = ?", name).First(&tag).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
tag = models.Tag{Name: name}
if err := tx.Create(&tag).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
} else {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
}
tags = append(tags, tag)
}
if err := tx.Model(&post).Association("Tags").Replace(&tags); err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
}
if err := tx.Commit().Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to commit"})
return
}
if err := database.DB.Preload("Categories").Preload("Tags").First(&post, post.ID).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
var catsResp []CategorySimple
for _, cc := range post.Categories {
catsResp = append(catsResp, CategorySimple{ID: cc.ID, Title: cc.Title, Slug: cc.Slug, ParentID: cc.ParentID})
}
var tagsResp []TagSimple
for _, t := range post.Tags {
tagsResp = append(tagsResp, TagSimple{ID: t.ID, Name: t.Name})
}
resp := PostResponse{ID: post.ID, Title: post.Title, Slug: post.Slug, Images: post.Images, Content: post.Content, Categories: catsResp, Tags: tagsResp, CreatedAt: post.CreatedAt, UpdatedAt: post.UpdatedAt}
c.JSON(http.StatusCreated, gin.H{"data": resp})
}