first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:43:40 +03:00
commit f34e54c5a5
100 changed files with 27342 additions and 0 deletions

View File

@@ -0,0 +1,235 @@
package handlers
import (
"gobeyhan/app/blog/services"
"gobeyhan/database/models"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
type CategoryHandler struct {
service *services.CategoryService
}
func NewCategoryHandler(service *services.CategoryService) *CategoryHandler {
return &CategoryHandler{service: service}
}
// GetAllCategories godoc
// @Summary Get all active categories
// @Description Get list of all active categories (public endpoint)
// @Tags categories
// @Accept json
// @Produce json
// @Success 200 {array} models.Category
// @Router /api/v1/categories [get]
func (h *CategoryHandler) GetAllCategories(c *gin.Context) {
categories, err := h.service.GetAllCategories(true)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": categories})
}
// GetCategoryBySlug godoc
// @Summary Get category by slug
// @Description Get a single category by its slug (public endpoint)
// @Tags categories
// @Accept json
// @Produce json
// @Param slug path string true "Category Slug"
// @Success 200 {object} models.Category
// @Router /api/v1/categories/{slug} [get]
func (h *CategoryHandler) GetCategoryBySlug(c *gin.Context) {
slug := c.Param("slug")
category, err := h.service.GetCategoryBySlug(slug)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if category == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Category not found"})
return
}
c.JSON(http.StatusOK, gin.H{"data": category})
}
// AdminGetAllCategories godoc
// @Summary Get all categories (Admin)
// @Description Get list of all categories including inactive ones
// @Tags admin,categories
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {array} models.Category
// @Router /api/v1/admin/categories [get]
func (h *CategoryHandler) AdminGetAllCategories(c *gin.Context) {
categories, err := h.service.GetAllCategories(false)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": categories})
}
// GetCategoryByID godoc
// @Summary Get category by ID (Admin)
// @Description Get a single category by ID
// @Tags admin,categories
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Category ID"
// @Success 200 {object} models.Category
// @Router /api/v1/admin/categories/{id} [get]
func (h *CategoryHandler) GetCategoryByID(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid category ID"})
return
}
category, err := h.service.GetCategoryByID(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if category == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Category not found"})
return
}
c.JSON(http.StatusOK, gin.H{"data": category})
}
// CreateCategory godoc
// @Summary Create a new category (Admin)
// @Description Create a new category
// @Tags admin,categories
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param category body models.Category true "Category object"
// @Success 201 {object} models.Category
// @Router /api/v1/admin/categories [post]
func (h *CategoryHandler) CreateCategory(c *gin.Context) {
var input struct {
Title string `json:"title" binding:"required"`
Keywords string `json:"keywords"`
Desc string `json:"description"`
IsActive *bool `json:"is_active"`
Order *int `json:"order"`
Slug string `json:"slug"`
ParentID *uint64 `json:"parent_id"`
Image string `json:"image"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
category := &models.Category{
Title: input.Title,
Keywords: input.Keywords,
Desc: input.Desc,
Slug: input.Slug,
ParentID: input.ParentID,
Image: input.Image,
}
if input.IsActive != nil {
category.IsActive = *input.IsActive
} else {
category.IsActive = true
}
if input.Order != nil {
category.Order = *input.Order
} else {
category.Order = 1
}
if err := h.service.CreateCategory(category); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"data": category})
}
// UpdateCategory godoc
// @Summary Update a category (Admin)
// @Description Update an existing category
// @Tags admin,categories
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Category ID"
// @Param category body models.Category true "Category object"
// @Success 200 {object} models.Category
// @Router /api/v1/admin/categories/{id} [put]
func (h *CategoryHandler) UpdateCategory(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid category ID"})
return
}
var input map[string]interface{}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := h.service.UpdateCategory(id, input); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Fetch updated category
category, err := h.service.GetCategoryByID(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": category})
}
// DeleteCategory godoc
// @Summary Delete a category (Admin)
// @Description Delete a category by ID
// @Tags admin,categories
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Category ID"
// @Success 200 {object} map[string]string
// @Router /api/v1/admin/categories/{id} [delete]
func (h *CategoryHandler) DeleteCategory(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid category ID"})
return
}
if err := h.service.DeleteCategory(id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Category deleted successfully"})
}

View File

@@ -0,0 +1,111 @@
package handlers
import (
"gobeyhan/app/blog/services"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
type CategoryViewHandler struct {
service *services.CategoryViewService
}
func NewCategoryViewHandler(service *services.CategoryViewService) *CategoryViewHandler {
return &CategoryViewHandler{service: service}
}
// TrackCategoryView godoc
// @Summary Track a category view
// @Description Record a view event for a category (public endpoint)
// @Tags category-views
// @Accept json
// @Produce json
// @Param id path int true "Category ID"
// @Success 200 {object} map[string]string
// @Router /api/v1/categories/{id}/view [post]
func (h *CategoryViewHandler) TrackCategoryView(c *gin.Context) {
idStr := c.Param("id")
categoryID, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid category ID"})
return
}
ipAddress := c.ClientIP()
userAgent := c.Request.UserAgent()
if err := h.service.TrackCategoryView(categoryID, ipAddress, userAgent); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "View tracked successfully"})
}
// AdminGetAllCategoryViews godoc
// @Summary Get all category views (Admin)
// @Description Get paginated list of all category views
// @Tags admin,category-views
// @Accept json
// @Produce json
// @Security BearerAuth
// @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{}
// @Router /api/v1/admin/category-views [get]
func (h *CategoryViewHandler) AdminGetAllCategoryViews(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
}
views, total, err := h.service.GetAllCategoryViews(page, limit)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"data": views,
"total": total,
"page": page,
"limit": limit,
})
}
// GetCategoryViewStats godoc
// @Summary Get view stats for a category (Admin)
// @Description Get view count and details for a specific category
// @Tags admin,category-views
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Category ID"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/admin/categories/{id}/views [get]
func (h *CategoryViewHandler) GetCategoryViewStats(c *gin.Context) {
idStr := c.Param("id")
categoryID, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid category ID"})
return
}
count, err := h.service.GetCategoryViewCount(categoryID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"category_id": categoryID,
"view_count": count,
})
}

View File

@@ -0,0 +1,245 @@
package handlers
import (
"gobeyhan/app/blog/services"
"gobeyhan/database/models"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
type CommentHandler struct {
service *services.CommentService
}
func NewCommentHandler(service *services.CommentService) *CommentHandler {
return &CommentHandler{service: service}
}
// GetPostComments godoc
// @Summary Get comments for a post
// @Description Get all active comments for a specific post (public endpoint)
// @Tags comments
// @Accept json
// @Produce json
// @Param id path int true "Post ID"
// @Success 200 {array} models.Comment
// @Router /api/v1/posts/{id}/comments [get]
func (h *CommentHandler) GetPostComments(c *gin.Context) {
idStr := c.Param("id")
postID, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid post ID"})
return
}
comments, err := h.service.GetCommentsByPost(postID, true)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": comments})
}
// CreatePostComment godoc
// @Summary Create a comment on a post
// @Description Create a new comment (requires authentication)
// @Tags comments
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Post ID"
// @Param comment body models.Comment true "Comment object"
// @Success 201 {object} models.Comment
// @Router /api/v1/posts/{id}/comments [post]
func (h *CommentHandler) CreatePostComment(c *gin.Context) {
idStr := c.Param("id")
postID, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid post ID"})
return
}
// Get user ID from context (set by auth middleware)
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
return
}
var input struct {
Title string `json:"title"`
Body string `json:"body" binding:"required"`
ParentID *uint64 `json:"parent_id"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Convert user_id to uint64
var uid uint64
switch v := userID.(type) {
case string:
uid, _ = strconv.ParseUint(v, 10, 64)
case uint64:
uid = v
case int:
uid = uint64(v)
case float64:
uid = uint64(v)
}
comment := &models.Comment{
UserID: uid,
ProductID: postID,
Title: input.Title,
Body: input.Body,
ParentID: input.ParentID,
IsActive: true,
}
if err := h.service.CreateComment(comment); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"data": comment})
}
// AdminGetAllComments godoc
// @Summary Get all comments (Admin)
// @Description Get paginated list of all comments
// @Tags admin,comments
// @Accept json
// @Produce json
// @Security BearerAuth
// @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{}
// @Router /api/v1/admin/comments [get]
func (h *CommentHandler) AdminGetAllComments(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
}
comments, total, err := h.service.GetAllComments(page, limit, false)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"data": comments,
"total": total,
"page": page,
"limit": limit,
})
}
// AdminGetCommentByID godoc
// @Summary Get comment by ID (Admin)
// @Description Get a single comment by ID
// @Tags admin,comments
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Comment ID"
// @Success 200 {object} models.Comment
// @Router /api/v1/admin/comments/{id} [get]
func (h *CommentHandler) AdminGetCommentByID(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid comment ID"})
return
}
comment, err := h.service.GetCommentByID(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if comment == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Comment not found"})
return
}
c.JSON(http.StatusOK, gin.H{"data": comment})
}
// AdminUpdateComment godoc
// @Summary Update a comment (Admin)
// @Description Update an existing comment
// @Tags admin,comments
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Comment ID"
// @Param comment body models.Comment true "Comment object"
// @Success 200 {object} models.Comment
// @Router /api/v1/admin/comments/{id} [put]
func (h *CommentHandler) AdminUpdateComment(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid comment ID"})
return
}
var input map[string]interface{}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := h.service.UpdateComment(id, input); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Fetch updated comment
comment, err := h.service.GetCommentByID(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": comment})
}
// AdminDeleteComment godoc
// @Summary Delete a comment (Admin)
// @Description Delete a comment by ID
// @Tags admin,comments
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Comment ID"
// @Success 200 {object} map[string]string
// @Router /api/v1/admin/comments/{id} [delete]
func (h *CommentHandler) AdminDeleteComment(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid comment ID"})
return
}
if err := h.service.DeleteComment(id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Comment deleted successfully"})
}

View File

@@ -0,0 +1,306 @@
package handlers
import (
"gobeyhan/app/blog/services"
"gobeyhan/database/models"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
type PostHandler struct {
service *services.PostService
}
func NewPostHandler(service *services.PostService) *PostHandler {
return &PostHandler{service: service}
}
// GetAllPosts godoc
// @Summary Get all active posts
// @Description Get paginated list of active posts (public endpoint)
// @Tags posts
// @Accept json
// @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{}
// @Router /api/v1/posts [get]
func (h *PostHandler) GetAllPosts(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
}
posts, total, err := h.service.GetAllPosts(page, limit, true)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"data": posts,
"total": total,
"page": page,
"limit": limit,
})
}
// GetPostBySlug godoc
// @Summary Get post by slug
// @Description Get a single post by its slug (public endpoint)
// @Tags posts
// @Accept json
// @Produce json
// @Param slug path string true "Post Slug"
// @Success 200 {object} models.Post
// @Router /api/v1/posts/{slug} [get]
func (h *PostHandler) GetPostBySlug(c *gin.Context) {
slug := c.Param("slug")
post, err := h.service.GetPostBySlug(slug)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if post == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Post not found"})
return
}
c.JSON(http.StatusOK, gin.H{"data": post})
}
// AdminGetAllPosts godoc
// @Summary Get all posts (Admin)
// @Description Get paginated list of all posts including inactive
// @Tags admin,posts
// @Accept json
// @Produce json
// @Security BearerAuth
// @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{}
// @Router /api/v1/admin/posts [get]
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
}
posts, total, err := h.service.GetAllPosts(page, limit, false)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"data": posts,
"total": total,
"page": page,
"limit": limit,
})
}
// GetPostByID godoc
// @Summary Get post by ID (Admin)
// @Description Get a single post by ID
// @Tags admin,posts
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Post ID"
// @Success 200 {object} models.Post
// @Router /api/v1/admin/posts/{id} [get]
func (h *PostHandler) GetPostByID(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid post ID"})
return
}
post, err := h.service.GetPostByID(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if post == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Post not found"})
return
}
c.JSON(http.StatusOK, gin.H{"data": post})
}
// CreatePost godoc
// @Summary Create a new post (Admin)
// @Description Create a new post
// @Tags admin,posts
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param post body models.Post true "Post object"
// @Success 201 {object} models.Post
// @Router /api/v1/admin/posts [post]
func (h *PostHandler) CreatePost(c *gin.Context) {
var input struct {
Title string `json:"title" binding:"required"`
UserID *uint64 `json:"user_id"`
Content string `json:"content"`
Keywords string `json:"keywords"`
Image string `json:"image"`
Thumb string `json:"thumb"`
Video string `json:"video"`
Slug string `json:"slug"`
IsActive *bool `json:"is_active"`
IsFront *bool `json:"is_front"`
ParentID *uint64 `json:"parent_id"`
Categories []uint64 `json:"categories"`
Tags []uint64 `json:"tags"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
post := &models.Post{
Title: input.Title,
UserID: input.UserID,
Content: input.Content,
Keywords: input.Keywords,
Image: input.Image,
Thumb: input.Thumb,
Video: input.Video,
Slug: input.Slug,
ParentID: input.ParentID,
}
if input.IsActive != nil {
post.IsActive = *input.IsActive
} else {
post.IsActive = true
}
if input.IsFront != nil {
post.IsFront = *input.IsFront
} else {
post.IsFront = true
}
// Create post first
if err := h.service.CreatePost(post); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Add categories and tags if provided
if len(input.Categories) > 0 || len(input.Tags) > 0 {
updates := make(map[string]interface{})
if len(input.Categories) > 0 {
var categories []*models.Category
for _, catID := range input.Categories {
categories = append(categories, &models.Category{ID: catID})
}
updates["categories"] = categories
}
if len(input.Tags) > 0 {
var tags []*models.Tag
for _, tagID := range input.Tags {
tags = append(tags, &models.Tag{ID: tagID})
}
updates["tags"] = tags
}
if err := h.service.UpdatePost(post.ID, updates); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
}
// Fetch created post with relationships
createdPost, _ := h.service.GetPostByID(post.ID)
c.JSON(http.StatusCreated, gin.H{"data": createdPost})
}
// UpdatePost godoc
// @Summary Update a post (Admin)
// @Description Update an existing post
// @Tags admin,posts
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Post ID"
// @Param post body models.Post true "Post object"
// @Success 200 {object} models.Post
// @Router /api/v1/admin/posts/{id} [put]
func (h *PostHandler) UpdatePost(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid post ID"})
return
}
var input map[string]interface{}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := h.service.UpdatePost(id, input); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Fetch updated post
post, err := h.service.GetPostByID(id)
if 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 (Admin)
// @Description Delete a post by ID
// @Tags admin,posts
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Post ID"
// @Success 200 {object} map[string]string
// @Router /api/v1/admin/posts/{id} [delete]
func (h *PostHandler) DeletePost(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid post ID"})
return
}
if err := h.service.DeletePost(id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Post deleted successfully"})
}

View File

@@ -0,0 +1,220 @@
package handlers
import (
"gobeyhan/app/blog/services"
"gobeyhan/database/models"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
type TagHandler struct {
service *services.TagService
}
func NewTagHandler(service *services.TagService) *TagHandler {
return &TagHandler{service: service}
}
// GetAllTags godoc
// @Summary Get all active tags
// @Description Get list of all active tags (public endpoint)
// @Tags tags
// @Accept json
// @Produce json
// @Success 200 {array} models.Tag
// @Router /api/v1/tags [get]
func (h *TagHandler) GetAllTags(c *gin.Context) {
tags, err := h.service.GetAllTags(true)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": tags})
}
// GetTagBySlug godoc
// @Summary Get tag by slug
// @Description Get a single tag by its slug (public endpoint)
// @Tags tags
// @Accept json
// @Produce json
// @Param slug path string true "Tag Slug"
// @Success 200 {object} models.Tag
// @Router /api/v1/tags/{slug} [get]
func (h *TagHandler) GetTagBySlug(c *gin.Context) {
slug := c.Param("slug")
tag, err := h.service.GetTagBySlug(slug)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if tag == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Tag not found"})
return
}
c.JSON(http.StatusOK, gin.H{"data": tag})
}
// AdminGetAllTags godoc
// @Summary Get all tags (Admin)
// @Description Get list of all tags including inactive ones
// @Tags admin,tags
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {array} models.Tag
// @Router /api/v1/admin/tags [get]
func (h *TagHandler) AdminGetAllTags(c *gin.Context) {
tags, err := h.service.GetAllTags(false)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": tags})
}
// GetTagByID godoc
// @Summary Get tag by ID (Admin)
// @Description Get a single tag by ID
// @Tags admin,tags
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Tag ID"
// @Success 200 {object} models.Tag
// @Router /api/v1/admin/tags/{id} [get]
func (h *TagHandler) GetTagByID(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid tag ID"})
return
}
tag, err := h.service.GetTagByID(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if tag == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Tag not found"})
return
}
c.JSON(http.StatusOK, gin.H{"data": tag})
}
// CreateTag godoc
// @Summary Create a new tag (Admin)
// @Description Create a new tag
// @Tags admin,tags
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param tag body models.Tag true "Tag object"
// @Success 201 {object} models.Tag
// @Router /api/v1/admin/tags [post]
func (h *TagHandler) CreateTag(c *gin.Context) {
var input struct {
Tag string `json:"tag" binding:"required"`
Slug string `json:"slug"`
IsActive *bool `json:"is_active"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
tag := &models.Tag{
Tag: input.Tag,
Slug: input.Slug,
}
if input.IsActive != nil {
tag.IsActive = *input.IsActive
} else {
tag.IsActive = true
}
if err := h.service.CreateTag(tag); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"data": tag})
}
// UpdateTag godoc
// @Summary Update a tag (Admin)
// @Description Update an existing tag
// @Tags admin,tags
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Tag ID"
// @Param tag body models.Tag true "Tag object"
// @Success 200 {object} models.Tag
// @Router /api/v1/admin/tags/{id} [put]
func (h *TagHandler) UpdateTag(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid tag ID"})
return
}
var input map[string]interface{}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := h.service.UpdateTag(id, input); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Fetch updated tag
tag, err := h.service.GetTagByID(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": tag})
}
// DeleteTag godoc
// @Summary Delete a tag (Admin)
// @Description Delete a tag by ID
// @Tags admin,tags
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Tag ID"
// @Success 200 {object} map[string]string
// @Router /api/v1/admin/tags/{id} [delete]
func (h *TagHandler) DeleteTag(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid tag ID"})
return
}
if err := h.service.DeleteTag(id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Tag deleted successfully"})
}

View File

@@ -0,0 +1,107 @@
package services
import (
"errors"
"gobeyhan/database"
"gobeyhan/database/models"
"gorm.io/gorm"
)
type CategoryService struct{}
func NewCategoryService() *CategoryService {
return &CategoryService{}
}
// GetAllCategories retrieves all categories, optionally filtering by active status
func (s *CategoryService) GetAllCategories(activeOnly bool) ([]models.Category, error) {
var categories []models.Category
query := database.DB.Preload("Parent").Preload("Children")
if activeOnly {
query = query.Where("is_active = ?", true)
}
err := query.Order("`order` ASC, created_at DESC").Find(&categories).Error
return categories, err
}
// GetCategoryByID retrieves a category by ID with parent and children relationships
func (s *CategoryService) GetCategoryByID(id uint64) (*models.Category, error) {
var category models.Category
err := database.DB.
Preload("Parent").
Preload("Children").
First(&category, id).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &category, nil
}
// GetCategoryBySlug retrieves a category by slug
func (s *CategoryService) GetCategoryBySlug(slug string) (*models.Category, error) {
var category models.Category
err := database.DB.
Preload("Parent").
Preload("Children").
Where("slug = ?", slug).
First(&category).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &category, nil
}
// CreateCategory creates a new category
func (s *CategoryService) CreateCategory(category *models.Category) error {
return database.DB.Create(category).Error
}
// UpdateCategory updates an existing category
func (s *CategoryService) UpdateCategory(id uint64, updates map[string]interface{}) error {
result := database.DB.Model(&models.Category{}).Where("id = ?", id).Updates(updates)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
// DeleteCategory deletes a category by ID
func (s *CategoryService) DeleteCategory(id uint64) error {
result := database.DB.Delete(&models.Category{}, id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
// GetCategoriesByParent retrieves child categories of a parent
func (s *CategoryService) GetCategoriesByParent(parentID uint64, activeOnly bool) ([]models.Category, error) {
var categories []models.Category
query := database.DB.Where("parent_id = ?", parentID)
if activeOnly {
query = query.Where("is_active = ?", true)
}
err := query.Order("`order` ASC, created_at DESC").Find(&categories).Error
return categories, err
}

View File

@@ -0,0 +1,67 @@
package services
import (
"gobeyhan/database"
"gobeyhan/database/models"
)
type CategoryViewService struct{}
func NewCategoryViewService() *CategoryViewService {
return &CategoryViewService{}
}
// TrackCategoryView records a category view
func (s *CategoryViewService) TrackCategoryView(categoryID uint64, ipAddress, userAgent string) error {
view := &models.CategoryView{
CategoryID: categoryID,
IPAddress: ipAddress,
UserAgent: userAgent,
}
return database.DB.Create(view).Error
}
// GetCategoryViewCount gets total view count for a category
func (s *CategoryViewService) GetCategoryViewCount(categoryID uint64) (int64, error) {
var count int64
err := database.DB.Model(&models.CategoryView{}).
Where("category_id = ?", categoryID).
Count(&count).Error
return count, err
}
// GetAllCategoryViews gets all views for a category with pagination
func (s *CategoryViewService) GetAllCategoryViews(page, limit int) ([]models.CategoryView, int64, error) {
var views []models.CategoryView
var total int64
query := database.DB.Preload("Category")
query.Model(&models.CategoryView{}).Count(&total)
err := query.
Offset((page - 1) * limit).
Limit(limit).
Order("created_at DESC").
Find(&views).Error
return views, total, err
}
// GetViewsByCategory gets views for a specific category
func (s *CategoryViewService) GetViewsByCategory(categoryID uint64, page, limit int) ([]models.CategoryView, int64, error) {
var views []models.CategoryView
var total int64
query := database.DB.Where("category_id = ?", categoryID)
query.Model(&models.CategoryView{}).Count(&total)
err := query.
Offset((page - 1) * limit).
Limit(limit).
Order("created_at DESC").
Find(&views).Error
return views, total, err
}

View File

@@ -0,0 +1,115 @@
package services
import (
"errors"
"gobeyhan/database"
"gobeyhan/database/models"
"gorm.io/gorm"
)
type CommentService struct{}
func NewCommentService() *CommentService {
return &CommentService{}
}
// GetCommentsByPost retrieves comments for a specific post
func (s *CommentService) GetCommentsByPost(postID uint64, activeOnly bool) ([]models.Comment, error) {
var comments []models.Comment
query := database.DB.
Where("product_id = ?", postID).
Preload("Parent").
Preload("Children")
if activeOnly {
query = query.Where("is_active = ?", true)
}
err := query.Order("created_at DESC").Find(&comments).Error
return comments, err
}
// GetAllComments retrieves all comments with pagination
func (s *CommentService) GetAllComments(page, limit int, activeOnly bool) ([]models.Comment, int64, error) {
var comments []models.Comment
var total int64
query := database.DB.
Preload("Product").
Preload("Parent").
Preload("Children")
if activeOnly {
query = query.Where("is_active = ?", true)
}
query.Model(&models.Comment{}).Count(&total)
err := query.
Offset((page - 1) * limit).
Limit(limit).
Order("created_at DESC").
Find(&comments).Error
return comments, total, err
}
// GetCommentByID retrieves a comment by ID
func (s *CommentService) GetCommentByID(id uint64) (*models.Comment, error) {
var comment models.Comment
err := database.DB.
Preload("Product").
Preload("Parent").
Preload("Children").
First(&comment, id).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &comment, nil
}
// CreateComment creates a new comment
func (s *CommentService) CreateComment(comment *models.Comment) error {
return database.DB.Create(comment).Error
}
// UpdateComment updates an existing comment
func (s *CommentService) UpdateComment(id uint64, updates map[string]interface{}) error {
result := database.DB.Model(&models.Comment{}).Where("id = ?", id).Updates(updates)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
// DeleteComment deletes a comment by ID
func (s *CommentService) DeleteComment(id uint64) error {
result := database.DB.Delete(&models.Comment{}, id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
// GetCommentReplies retrieves replies to a specific comment
func (s *CommentService) GetCommentReplies(commentID uint64) ([]models.Comment, error) {
var replies []models.Comment
err := database.DB.
Where("parent_id = ? AND is_active = ?", commentID, true).
Order("created_at ASC").
Find(&replies).Error
return replies, err
}

View File

@@ -0,0 +1,202 @@
package services
import (
"errors"
"gobeyhan/database"
"gobeyhan/database/models"
"gorm.io/gorm"
)
type PostService struct{}
func NewPostService() *PostService {
return &PostService{}
}
// GetAllPosts retrieves all posts with pagination, optionally filtering by active status
func (s *PostService) GetAllPosts(page, limit int, activeOnly bool) ([]models.Post, int64, error) {
var posts []models.Post
var total int64
query := database.DB.
Preload("User").
Preload("Categories").
Preload("Tags").
Preload("Parent").
Preload("Children")
if activeOnly {
query = query.Where("is_active = ?", true)
}
// Count total
query.Model(&models.Post{}).Count(&total)
// Get paginated results
err := query.
Offset((page - 1) * limit).
Limit(limit).
Order("created_at DESC").
Find(&posts).Error
return posts, total, err
}
// GetPostByID retrieves a post by ID with all relationships
func (s *PostService) GetPostByID(id uint64) (*models.Post, error) {
var post models.Post
err := database.DB.
Preload("User").
Preload("Categories").
Preload("Tags").
Preload("Parent").
Preload("Children").
First(&post, id).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &post, nil
}
// GetPostBySlug retrieves a post by slug
func (s *PostService) GetPostBySlug(slug string) (*models.Post, error) {
var post models.Post
err := database.DB.
Preload("User").
Preload("Categories").
Preload("Tags").
Preload("Parent").
Preload("Children").
Where("slug = ? AND is_active = ?", slug, true).
First(&post).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &post, nil
}
// CreatePost creates a new post
func (s *PostService) CreatePost(post *models.Post) error {
return database.DB.Create(post).Error
}
// UpdatePost updates an existing post
func (s *PostService) UpdatePost(id uint64, updates map[string]interface{}) error {
// Handle many-to-many relationships separately if they're in updates
var categoryIDs []*models.Category
var tagIDs []*models.Tag
if categories, ok := updates["categories"]; ok {
if catSlice, ok := categories.([]*models.Category); ok {
categoryIDs = catSlice
delete(updates, "categories")
}
}
if tags, ok := updates["tags"]; ok {
if tagSlice, ok := tags.([]*models.Tag); ok {
tagIDs = tagSlice
delete(updates, "tags")
}
}
// Update basic fields
result := database.DB.Model(&models.Post{}).Where("id = ?", id).Updates(updates)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
// Update relationships if provided
if len(categoryIDs) > 0 || len(tagIDs) > 0 {
var post models.Post
if err := database.DB.First(&post, id).Error; err != nil {
return err
}
if len(categoryIDs) > 0 {
if err := database.DB.Model(&post).Association("Categories").Replace(categoryIDs); err != nil {
return err
}
}
if len(tagIDs) > 0 {
if err := database.DB.Model(&post).Association("Tags").Replace(tagIDs); err != nil {
return err
}
}
}
return nil
}
// DeletePost deletes a post by ID
func (s *PostService) DeletePost(id uint64) error {
result := database.DB.Delete(&models.Post{}, id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
// GetPostsByCategory retrieves posts by category ID
func (s *PostService) GetPostsByCategory(categoryID uint64, page, limit int) ([]models.Post, int64, error) {
var posts []models.Post
var total int64
query := database.DB.
Joins("JOIN post_categories ON post_categories.post_id = posts.id").
Where("post_categories.category_id = ? AND posts.is_active = ?", categoryID, true).
Preload("User").
Preload("Categories").
Preload("Tags")
query.Model(&models.Post{}).Count(&total)
err := query.
Offset((page - 1) * limit).
Limit(limit).
Order("posts.created_at DESC").
Find(&posts).Error
return posts, total, err
}
// GetPostsByTag retrieves posts by tag ID
func (s *PostService) GetPostsByTag(tagID uint64, page, limit int) ([]models.Post, int64, error) {
var posts []models.Post
var total int64
query := database.DB.
Joins("JOIN post_tags ON post_tags.post_id = posts.id").
Where("post_tags.tag_id = ? AND posts.is_active = ?", tagID, true).
Preload("User").
Preload("Categories").
Preload("Tags")
query.Model(&models.Post{}).Count(&total)
err := query.
Offset((page - 1) * limit).
Limit(limit).
Order("posts.created_at DESC").
Find(&posts).Error
return posts, total, err
}

View File

@@ -0,0 +1,87 @@
package services
import (
"errors"
"gobeyhan/database"
"gobeyhan/database/models"
"gorm.io/gorm"
)
type TagService struct{}
func NewTagService() *TagService {
return &TagService{}
}
// GetAllTags retrieves all tags, optionally filtering by active status
func (s *TagService) GetAllTags(activeOnly bool) ([]models.Tag, error) {
var tags []models.Tag
query := database.DB
if activeOnly {
query = query.Where("is_active = ?", true)
}
err := query.Order("tag ASC").Find(&tags).Error
return tags, err
}
// GetTagByID retrieves a tag by ID
func (s *TagService) GetTagByID(id uint64) (*models.Tag, error) {
var tag models.Tag
err := database.DB.First(&tag, id).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &tag, nil
}
// GetTagBySlug retrieves a tag by slug
func (s *TagService) GetTagBySlug(slug string) (*models.Tag, error) {
var tag models.Tag
err := database.DB.Where("slug = ?", slug).First(&tag).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &tag, nil
}
// CreateTag creates a new tag
func (s *TagService) CreateTag(tag *models.Tag) error {
return database.DB.Create(tag).Error
}
// UpdateTag updates an existing tag
func (s *TagService) UpdateTag(id uint64, updates map[string]interface{}) error {
result := database.DB.Model(&models.Tag{}).Where("id = ?", id).Updates(updates)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
// DeleteTag deletes a tag by ID
func (s *TagService) DeleteTag(id uint64) error {
result := database.DB.Delete(&models.Tag{}, id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}