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

539 lines
16 KiB
Go

package controllers
import (
"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"
)
// Hero payload
type HeroPayload struct {
Color string `json:"color" binding:"required"`
Title string `json:"title"`
Text1 string `json:"text1"`
Text2 string `json:"text2"`
Text4 string `json:"text4"`
Text5 string `json:"text5"`
Image string `json:"image"`
IsActive *bool `json:"is_active"`
}
// AdminListHeroes godoc
// @Summary Admin: List heroes
// @Description Admin listing of heroes. Use ?soft=only to list deleted, ?soft=with to include deleted.
// @Tags heroes
// @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} controllers.HeroListResponse
// @Failure 500 {object} map[string]string
// @Router /api/v1/admin/heroes [get]
func AdminListHeroes(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.Hero{}).Where("deleted_at IS NOT NULL")
} else if soft == "with" {
query = database.DB.Unscoped().Model(&models.Hero{})
} else {
query = database.DB.Model(&models.Hero{})
}
var total int64
if err := query.Count(&total).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
var items []models.Hero
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
}
c.JSON(http.StatusOK, gin.H{"items": items, "total": total, "page": page, "per_page": perPage})
}
// AdminGetHero godoc
// @Summary Admin: Get a hero by id
// @Description Return a single hero by id
// @Tags heroes
// @Security BearerAuth
// @Produce json
// @Param id path int true "Hero ID"
// @Success 200 {object} controllers.HeroResponse
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /api/v1/admin/heroes/{id} [get]
func AdminGetHero(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 h models.Hero
if err := database.DB.Unscoped().First(&h, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "hero not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": h})
}
// CreateHero godoc
// @Summary Admin: Create a hero
// @Description Create a new hero item (multipart/form-data)
// @Tags heroes
// @Security BearerAuth
// @Accept multipart/form-data
// @Produce json
// @Param color formData string true "Color"
// @Param title formData string false "Title"
// @Param text1 formData string false "Text1"
// @Param text2 formData string false "Text2"
// @Param text4 formData string false "Text4"
// @Param text5 formData string false "Text5"
// @Param is_active formData boolean false "Is Active"
// @Param width formData int false "Image width (frontend-provided)"
// @Param height formData int false "Image height (frontend-provided)"
// @Param quality formData int false "Image quality (frontend-provided)"
// @Param format formData string false "Image format (jpeg|png|webp) (frontend-provided)"
// @Param image formData file false "Image file"
// @Success 201 {object} controllers.HeroResponse
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/v1/admin/heroes [post]
func CreateHero(c *gin.Context) {
if database.DB == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"})
return
}
// Parse form fields
color := c.PostForm("color")
if color == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "color is required"})
return
}
title := c.PostForm("title")
text1 := c.PostForm("text1")
text2 := c.PostForm("text2")
text4 := c.PostForm("text4")
text5 := c.PostForm("text5")
isActive := true
if v := c.PostForm("is_active"); v != "" {
if b, err := strconv.ParseBool(v); err == nil {
isActive = b
}
}
// optional frontend-provided image metadata
var width, height, quality int
if w := c.PostForm("width"); w != "" {
if wi, err := strconv.Atoi(w); err == nil {
width = wi
}
}
if h := c.PostForm("height"); h != "" {
if hi, err := strconv.Atoi(h); err == nil {
height = hi
}
}
if q := c.PostForm("quality"); q != "" {
if qi, err := strconv.Atoi(q); err == nil {
quality = qi
}
}
format := c.PostForm("format")
hero := models.Hero{
Color: color,
Title: title,
Text1: text1,
Text2: text2,
Text4: text4,
Text5: text5,
IsActive: isActive,
Width: width,
Height: height,
Quality: quality,
Format: format,
}
// handle file upload (no server-side image processing)
file, err := c.FormFile("image")
if err == nil {
// ensure uploads/heroes exists
uploadDir := filepath.Join("uploads", "heroes")
if err := os.MkdirAll(uploadDir, os.ModePerm); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create upload dir"})
return
}
ext := filepath.Ext(file.Filename)
newName := fmt.Sprintf("hero-%d%s", time.Now().UnixNano(), ext)
destination := filepath.Join(uploadDir, newName)
if err := c.SaveUploadedFile(file, destination); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
hero.Image = "/uploads/heroes/" + newName
// do not attempt to decode/process image here; frontend provides metadata
// if format not provided, fallback to extension without dot
if heroFormat := format; heroFormat == "" {
if ext != "" {
hero.Format = ext[1:]
}
}
}
if err := database.DB.Create(&hero).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"data": hero})
}
// UpdateHero godoc
// @Summary Admin: Update a hero
// @Description Update an existing hero (multipart/form-data)
// @Tags heroes
// @Security BearerAuth
// @Accept multipart/form-data
// @Produce json
// @Param id path int true "Hero ID"
// @Param color formData string false "Color"
// @Param title formData string false "Title"
// @Param text1 formData string false "Text1"
// @Param text2 formData string false "Text2"
// @Param text4 formData string false "Text4"
// @Param text5 formData string false "Text5"
// @Param is_active formData boolean false "Is Active"
// @Param width formData int false "Image width (frontend-provided)"
// @Param height formData int false "Image height (frontend-provided)"
// @Param quality formData int false "Image quality (frontend-provided)"
// @Param format formData string false "Image format (jpeg|png|webp) (frontend-provided)"
// @Param image formData file false "Image file"
// @Success 200 {object} controllers.HeroResponse
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/v1/admin/heroes/{id} [put]
func UpdateHero(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 h models.Hero
if err := database.DB.First(&h, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "hero not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// read form fields (if present)
if color := c.PostForm("color"); color != "" {
h.Color = color
}
if title := c.PostForm("title"); title != "" {
h.Title = title
}
if t := c.PostForm("text1"); t != "" {
h.Text1 = t
}
if t := c.PostForm("text2"); t != "" {
h.Text2 = t
}
if t := c.PostForm("text4"); t != "" {
h.Text4 = t
}
if t := c.PostForm("text5"); t != "" {
h.Text5 = t
}
if v := c.PostForm("is_active"); v != "" {
if b, err := strconv.ParseBool(v); err == nil {
h.IsActive = b
}
}
// optional frontend-provided image metadata
if w := c.PostForm("width"); w != "" {
if wi, err := strconv.Atoi(w); err == nil {
h.Width = wi
}
}
if hgt := c.PostForm("height"); hgt != "" {
if hi, err := strconv.Atoi(hgt); err == nil {
h.Height = hi
}
}
if q := c.PostForm("quality"); q != "" {
if qi, err := strconv.Atoi(q); err == nil {
h.Quality = qi
}
}
if fmtStr := c.PostForm("format"); fmtStr != "" {
h.Format = fmtStr
}
// handle optional file upload (no server-side processing)
file, err := c.FormFile("image")
if err == nil {
// Save new file first
uploadDir := filepath.Join("uploads", "heroes")
if err := os.MkdirAll(uploadDir, os.ModePerm); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create upload dir"})
return
}
ext := filepath.Ext(file.Filename)
newName := fmt.Sprintf("hero-%d%s", time.Now().UnixNano(), ext)
destination := filepath.Join(uploadDir, newName)
if err := c.SaveUploadedFile(file, destination); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// If there was a previous image, attempt to remove it safely
prev := h.Image
if prev != "" {
// normalize and ensure it's inside uploads/
prevPath := strings.TrimPrefix(prev, "/")
clean := filepath.Clean(prevPath)
// only remove files under uploads/ to avoid accidental deletions
if strings.HasPrefix(clean, "uploads"+string(os.PathSeparator)) {
_ = os.Remove(clean) // ignore error
}
}
h.Image = "/uploads/heroes/" + newName
// if format not provided by frontend, fallback to extension
if h.Format == "" && ext != "" {
h.Format = ext[1:]
}
}
if err := database.DB.Save(&h).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": h})
}
// DeleteHero godoc
// @Summary Admin: Delete a hero
// @Description Soft-delete a hero by ID
// @Tags heroes
// @Security BearerAuth
// @Produce json
// @Param id path int true "Hero 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/heroes/{id} [delete]
func DeleteHero(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 h models.Hero
if err := database.DB.First(&h, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "hero not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if err := database.DB.Delete(&h).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// attempt to remove image file if present
if h.Image != "" {
imgPath := strings.TrimPrefix(h.Image, "/")
clean := filepath.Clean(imgPath)
if strings.HasPrefix(clean, "uploads"+string(os.PathSeparator)) {
_ = os.Remove(clean)
}
}
c.JSON(http.StatusOK, gin.H{"message": "hero deleted successfully", "id": h.ID})
}
// RestoreHero godoc
// @Summary Admin: Restore a soft-deleted hero
// @Description Restore a soft-deleted hero by ID
// @Tags heroes
// @Security BearerAuth
// @Produce json
// @Param id path int true "Hero ID"
// @Success 200 {object} controllers.HeroResponse
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/v1/admin/heroes/{id}/restore [post]
func RestoreHero(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 h models.Hero
// Find soft-deleted record with Unscoped
if err := database.DB.Unscoped().Where("id = ?", id).First(&h).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "hero not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Clear deleted_at (restore)
if err := database.DB.Unscoped().Model(&models.Hero{}).Where("id = ?", id).Update("deleted_at", nil).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Reload the record in normal scope to ensure DeletedAt is nil in struct
if err := database.DB.First(&h, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": h})
}
// ListHeroes godoc
// @Summary Public: List heroes
// @Description Return active heroes with pagination
// @Tags heroes
// @Produce json
// @Param page query int false "Page number"
// @Param per_page query int false "Items per page"
// @Success 200 {object} controllers.HeroListResponse
// @Failure 500 {object} map[string]string
// @Router /api/v1/heroes [get]
func ListHeroes(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.Hero{}).Where("is_active = ?", true)
var total int64
if err := query.Count(&total).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
var items []models.Hero
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
}
c.JSON(http.StatusOK, gin.H{"items": items, "total": total, "page": page, "per_page": perPage})
}
// GetHero godoc
// @Summary Public: Get a hero by id
// @Description Return a single hero by id
// @Tags heroes
// @Produce json
// @Param id path int true "Hero ID"
// @Success 200 {object} controllers.HeroResponse
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /api/v1/heroes/{id} [get]
func GetHero(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 h models.Hero
if err := database.DB.First(&h, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "hero not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": h})
}