539 lines
16 KiB
Go
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})
|
|
}
|