1736 lines
53 KiB
Go
1736 lines
53 KiB
Go
package controllers
|
||
|
||
// NOTE: minor edit test to verify file is writable
|
||
|
||
import (
|
||
configs "ares/config"
|
||
dbConfig "ares/database/config"
|
||
"ares/database/models"
|
||
"ares/middlewares"
|
||
utils "ares/pkg/utis"
|
||
"ares/services"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"io"
|
||
"math"
|
||
"net/http"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"gorm.io/gorm"
|
||
|
||
"github.com/gofiber/fiber/v3"
|
||
"go.uber.org/zap"
|
||
"golang.org/x/crypto/bcrypt"
|
||
)
|
||
|
||
// parseImagesField accepts the stored Images field which may be either a JSON
|
||
// array string like '["/uploads/..."]' or a plain string "/uploads/..." and
|
||
// returns a slice of image paths.
|
||
func parseImagesField(s string) []string {
|
||
var imgs []string
|
||
s = strings.TrimSpace(s)
|
||
if s == "" {
|
||
return imgs
|
||
}
|
||
// If it looks like a JSON array attempt to unmarshal
|
||
if strings.HasPrefix(s, "[") {
|
||
if err := json.Unmarshal([]byte(s), &imgs); err == nil {
|
||
return imgs
|
||
}
|
||
}
|
||
// Otherwise treat as a single path string (strip surrounding quotes if present)
|
||
s = strings.Trim(s, "\"")
|
||
if s != "" {
|
||
imgs = append(imgs, s)
|
||
}
|
||
return imgs
|
||
}
|
||
|
||
// AdminLogin renders the login page
|
||
func AdminLogin(c fiber.Ctx) error {
|
||
return c.Render("admin/login", fiber.Map{})
|
||
}
|
||
|
||
// AdminLoginPost handles the login form submission
|
||
func AdminLoginPost(c fiber.Ctx) error {
|
||
email := c.FormValue("email")
|
||
password := c.FormValue("password")
|
||
turnstileToken := c.FormValue("cf-turnstile-response")
|
||
|
||
// 1. Verify Turnstile Token
|
||
if turnstileToken == "" {
|
||
// return c.Status(fiber.StatusBadRequest).SendString("<div class='alert alert-danger'>Turnstile doğrulaması başarısız.</div>")
|
||
}
|
||
|
||
// 2. Verify Credentials against Database (include profile)
|
||
var user models.User
|
||
if err := dbConfig.DB.Preload("Profile").Where("email = ?", email).First(&user).Error; err != nil {
|
||
return c.Status(fiber.StatusOK).SendString("<div class='alert alert-danger'>Hatalı e-posta veya şifre.</div>")
|
||
}
|
||
|
||
// Check password
|
||
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
|
||
return c.Status(fiber.StatusOK).SendString("<div class='alert alert-danger'>Hatalı e-posta veya şifre.</div>")
|
||
}
|
||
|
||
// Check if admin
|
||
if user.IsAdmin == nil || !*user.IsAdmin {
|
||
return c.Status(fiber.StatusOK).SendString("<div class='alert alert-danger'>Bu alana erişim yetkiniz yok.</div>")
|
||
}
|
||
|
||
// Login success - generate signed JWT cookie
|
||
jwtService := services.NewJWTService()
|
||
first := ""
|
||
last := ""
|
||
if len(user.Profile) > 0 {
|
||
first = user.Profile[0].FirstName
|
||
last = user.Profile[0].LastName
|
||
}
|
||
accessToken, _, err := jwtService.GenerateTokenPair(uint(user.ID), user.Email, user.IsAdmin != nil && *user.IsAdmin, first, last)
|
||
if err != nil {
|
||
return c.Status(fiber.StatusInternalServerError).SendString("<div class='alert alert-danger'>Oturum oluşturulamadı.</div>")
|
||
}
|
||
|
||
cookie := new(fiber.Cookie)
|
||
cookie.Name = "admin_session"
|
||
cookie.Value = accessToken
|
||
cookie.Expires = time.Now().Add(time.Duration(configs.AppConfig.AccessTokenExpireMinutes) * time.Minute)
|
||
cookie.Path = "/"
|
||
cookie.HTTPOnly = true
|
||
cookie.Secure = true
|
||
cookie.SameSite = "Strict"
|
||
c.Cookie(cookie)
|
||
|
||
// Check if request is from HTMX
|
||
if c.Get("HX-Request") == "true" {
|
||
c.Set("HX-Redirect", "/admin")
|
||
return c.SendStatus(fiber.StatusOK)
|
||
}
|
||
|
||
// Standard redirect for non-HTMX requests (fallback)
|
||
return c.Redirect().To("/admin")
|
||
}
|
||
|
||
// AdminLogout clears the session
|
||
func AdminLogout(c fiber.Ctx) error {
|
||
c.ClearCookie("admin_session")
|
||
return c.Redirect().To("/login")
|
||
}
|
||
|
||
// AdminDashboard renders the full layout with dashboard content
|
||
func AdminDashboard(c fiber.Ctx) error {
|
||
return c.Render("admin/partials/dashboard", fiber.Map{
|
||
"Title": "Dashboard",
|
||
}, "admin/layout")
|
||
}
|
||
|
||
// AdminContentDashboard renders the dashboard partial (for HTMX) or full page
|
||
func AdminContentDashboard(c fiber.Ctx) error {
|
||
if c.Get("HX-Request") == "true" {
|
||
return c.Render("admin/partials/dashboard", fiber.Map{})
|
||
}
|
||
return c.Render("admin/partials/dashboard", fiber.Map{
|
||
"Title": "Dashboard",
|
||
}, "admin/layout")
|
||
}
|
||
|
||
// AdminMe returns basic info about the currently authenticated admin (name, email, avatar)
|
||
func AdminMe(c fiber.Ctx) error {
|
||
claims, ok := middlewares.GetAuthClaims(c)
|
||
if !ok {
|
||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "unauthorized"})
|
||
}
|
||
|
||
name := strings.TrimSpace(claims.FirstName + " " + claims.LastName)
|
||
if name == "" {
|
||
name = claims.Email
|
||
}
|
||
|
||
avatar := ""
|
||
var user models.User
|
||
if err := dbConfig.DB.Preload("Profile").First(&user, claims.UserID).Error; err == nil {
|
||
if len(user.Profile) > 0 {
|
||
avatar = user.Profile[0].AvatarURL
|
||
}
|
||
}
|
||
|
||
return c.JSON(fiber.Map{"name": name, "email": claims.Email, "avatar": avatar})
|
||
}
|
||
|
||
// AdminContentUsers renders the users partial with pagination and search
|
||
func AdminContentUsers(c fiber.Ctx) error {
|
||
page, _ := strconv.Atoi(c.Query("page", "1"))
|
||
limit := 10
|
||
offset := (page - 1) * limit
|
||
search := c.Query("search", "")
|
||
showDeleted := c.Query("deleted") == "true"
|
||
|
||
var users []models.User
|
||
var total int64
|
||
|
||
query := dbConfig.DB.Model(&models.User{})
|
||
|
||
if showDeleted {
|
||
query = query.Unscoped().Where("deleted_at IS NOT NULL")
|
||
}
|
||
|
||
if search != "" {
|
||
query = query.Where("user_name LIKE ? OR email LIKE ?", "%"+search+"%", "%"+search+"%")
|
||
}
|
||
|
||
query.Count(&total)
|
||
query.Limit(limit).Offset(offset).Order("created_at desc").Find(&users)
|
||
|
||
totalPages := int(math.Ceil(float64(total) / float64(limit)))
|
||
|
||
// Check if request is from HTMX
|
||
if c.Get("HX-Request") == "true" {
|
||
return c.Render("admin/partials/users", fiber.Map{
|
||
"Users": users,
|
||
"Page": page,
|
||
"TotalPages": totalPages,
|
||
"NextPage": page + 1,
|
||
"PrevPage": page - 1,
|
||
"Search": search,
|
||
"ShowDeleted": showDeleted,
|
||
})
|
||
}
|
||
|
||
// If not HTMX (e.g. page refresh), render with layout
|
||
return c.Render("admin/partials/users", fiber.Map{
|
||
"Users": users,
|
||
"Page": page,
|
||
"TotalPages": totalPages,
|
||
"NextPage": page + 1,
|
||
"PrevPage": page - 1,
|
||
"Search": search,
|
||
"ShowDeleted": showDeleted,
|
||
}, "admin/layout")
|
||
}
|
||
|
||
// AdminUserNew renders the create user full page form
|
||
func AdminUserNew(c fiber.Ctx) error {
|
||
return c.Render("admin/pages/user_form", fiber.Map{
|
||
"IsEdit": false,
|
||
}, "admin/layout")
|
||
}
|
||
|
||
// AdminUserCreate handles user creation
|
||
func AdminUserCreate(c fiber.Ctx) error {
|
||
username := c.FormValue("username")
|
||
email := c.FormValue("email")
|
||
password := c.FormValue("password")
|
||
isAdmin := c.FormValue("is_admin") == "on"
|
||
emailVerified := c.FormValue("email_verified") == "on"
|
||
|
||
// Basic validation
|
||
if username == "" || email == "" || password == "" {
|
||
return c.Status(fiber.StatusBadRequest).SendString("<div class='alert alert-danger'>Lütfen tüm alanları doldurun.</div>")
|
||
}
|
||
|
||
// Hash password
|
||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||
if err != nil {
|
||
return c.Status(fiber.StatusInternalServerError).SendString("<div class='alert alert-danger'>Şifre oluşturulurken hata.</div>")
|
||
}
|
||
|
||
user := models.User{
|
||
UserName: username,
|
||
Email: email,
|
||
Password: string(hashedPassword),
|
||
IsAdmin: &isAdmin,
|
||
EmailVerified: &emailVerified,
|
||
}
|
||
|
||
if err := dbConfig.DB.Create(&user).Error; err != nil {
|
||
return c.Status(fiber.StatusInternalServerError).SendString("<div class='alert alert-danger'>Kullanıcı oluşturulurken hata: " + err.Error() + "</div>")
|
||
}
|
||
|
||
// If profile fields or avatar provided, process and create profile
|
||
firstName := c.FormValue("first_name")
|
||
lastName := c.FormValue("last_name")
|
||
avatarPath := ""
|
||
if p, err := services.ProcessAndSaveImage(c, "avatar", services.ImageOptions{
|
||
Width: 150,
|
||
Height: 150,
|
||
Quality: 85,
|
||
Format: "avif",
|
||
Folder: "avatars",
|
||
}); err == nil {
|
||
avatarPath = p
|
||
}
|
||
|
||
if firstName != "" || lastName != "" || avatarPath != "" {
|
||
profile := models.Profile{UserID: uint64(user.ID)}
|
||
if firstName != "" {
|
||
profile.FirstName = firstName
|
||
}
|
||
if lastName != "" {
|
||
profile.LastName = lastName
|
||
}
|
||
if avatarPath != "" {
|
||
profile.AvatarURL = avatarPath
|
||
}
|
||
dbConfig.DB.Create(&profile)
|
||
}
|
||
|
||
return c.Redirect().To("/admin/content/users?success=Kullanıcı+başarıyla+oluşturuldu")
|
||
}
|
||
|
||
// AdminUserEdit renders the edit user full page form
|
||
func AdminUserEdit(c fiber.Ctx) error {
|
||
id := c.Params("id")
|
||
var user models.User
|
||
if err := dbConfig.DB.Preload("Profile").First(&user, id).Error; err != nil {
|
||
return c.Status(fiber.StatusNotFound).SendString("Kullanıcı bulunamadı")
|
||
}
|
||
|
||
return c.Render("admin/pages/user_form", fiber.Map{
|
||
"IsEdit": true,
|
||
"User": user,
|
||
}, "admin/layout")
|
||
}
|
||
|
||
// AdminUserUpdate handles user update
|
||
func AdminUserUpdate(c fiber.Ctx) error {
|
||
id := c.Params("id")
|
||
var user models.User
|
||
if err := dbConfig.DB.Preload("Profile").First(&user, id).Error; err != nil {
|
||
return c.Status(fiber.StatusNotFound).SendString("Kullanıcı bulunamadı")
|
||
}
|
||
|
||
user.UserName = c.FormValue("username")
|
||
user.Email = c.FormValue("email")
|
||
|
||
isAdmin := c.FormValue("is_admin") == "on"
|
||
user.IsAdmin = &isAdmin
|
||
|
||
emailVerified := c.FormValue("email_verified") == "on"
|
||
user.EmailVerified = &emailVerified
|
||
|
||
// Update password only if provided
|
||
newPassword := c.FormValue("password")
|
||
if newPassword != "" {
|
||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
|
||
if err != nil {
|
||
return c.Status(fiber.StatusInternalServerError).SendString("<div class='alert alert-danger'>Şifre güncellenirken hata.</div>")
|
||
}
|
||
user.Password = string(hashedPassword)
|
||
}
|
||
|
||
if err := dbConfig.DB.Save(&user).Error; err != nil {
|
||
return c.Status(fiber.StatusInternalServerError).SendString("<div class='alert alert-danger'>Güncelleme hatası: " + err.Error() + "</div>")
|
||
}
|
||
|
||
// Handle profile fields and avatar upload
|
||
firstName := c.FormValue("first_name")
|
||
lastName := c.FormValue("last_name")
|
||
|
||
// Avatar file handling via image service (150x150, AVIF)
|
||
avatarPath := ""
|
||
if p, err := services.ProcessAndSaveImage(c, "avatar", services.ImageOptions{
|
||
Width: 150,
|
||
Height: 150,
|
||
Quality: 85,
|
||
Format: "avif",
|
||
Folder: "avatars",
|
||
}); err == nil {
|
||
avatarPath = p
|
||
}
|
||
|
||
// Update or create profile
|
||
if len(user.Profile) > 0 {
|
||
profile := user.Profile[0]
|
||
if firstName != "" {
|
||
profile.FirstName = firstName
|
||
}
|
||
if lastName != "" {
|
||
profile.LastName = lastName
|
||
}
|
||
if avatarPath != "" {
|
||
profile.AvatarURL = avatarPath
|
||
}
|
||
if err := dbConfig.DB.Model(&profile).Updates(profile).Error; err != nil {
|
||
// continue but log error
|
||
}
|
||
} else {
|
||
// create
|
||
newProfile := models.Profile{UserID: uint64(user.ID)}
|
||
if firstName != "" {
|
||
newProfile.FirstName = firstName
|
||
}
|
||
if lastName != "" {
|
||
newProfile.LastName = lastName
|
||
}
|
||
if avatarPath != "" {
|
||
newProfile.AvatarURL = avatarPath
|
||
}
|
||
if err := dbConfig.DB.Create(&newProfile).Error; err != nil {
|
||
// continue
|
||
}
|
||
}
|
||
|
||
return c.Redirect().To("/admin/content/users?success=Kullanıcı+başarıyla+güncellendi")
|
||
}
|
||
|
||
// AdminUserDelete handles user soft delete
|
||
func AdminUserDelete(c fiber.Ctx) error {
|
||
id := c.Params("id")
|
||
if err := dbConfig.DB.Delete(&models.User{}, id).Error; err != nil {
|
||
return c.Status(fiber.StatusInternalServerError).SendString("Silme hatası")
|
||
}
|
||
|
||
// Return updated list or trigger
|
||
return c.Redirect().To("/admin/content/users?success=Kullanıcı+silindi")
|
||
}
|
||
|
||
// AdminUserRestore restores a soft-deleted user
|
||
func AdminUserRestore(c fiber.Ctx) error {
|
||
id := c.Params("id")
|
||
if err := dbConfig.DB.Unscoped().Model(&models.User{}).Where("id = ?", id).Update("deleted_at", nil).Error; err != nil {
|
||
return c.Status(fiber.StatusInternalServerError).SendString("Geri yükleme hatası")
|
||
}
|
||
|
||
return c.Redirect().To("/admin/content/users?deleted=true&success=Kullanıcı+geri+yüklendi")
|
||
}
|
||
|
||
// AdminContentSettings renders the settings partial with General Settings and Hero list
|
||
func AdminContentSettings(c fiber.Ctx) error {
|
||
// 1. Fetch General Settings
|
||
var setting models.Setting
|
||
if err := dbConfig.DB.First(&setting).Error; err != nil {
|
||
// If not found, create default? Or just empty.
|
||
// For now, empty is fine, UI should handle it.
|
||
}
|
||
|
||
// 2. Fetch Heroes
|
||
var heroes []models.Hero
|
||
showDeleted := c.Query("deleted") == "true"
|
||
query := dbConfig.DB.Model(&models.Hero{})
|
||
|
||
if showDeleted {
|
||
query = query.Unscoped().Where("deleted_at IS NOT NULL")
|
||
}
|
||
|
||
query.Order("created_at desc").Find(&heroes)
|
||
|
||
// 3. Fetch CORS lists and rate limits
|
||
var corsWhitelist []models.CorsWhitelist
|
||
var corsBlacklist []models.CorsBlacklist
|
||
var rateLimits []models.RateLimitSetting
|
||
|
||
// Respect showDeleted flag for soft-deleted items
|
||
if showDeleted {
|
||
dbConfig.DB.Unscoped().Where("deleted_at IS NOT NULL").Order("created_at desc").Find(&corsWhitelist)
|
||
dbConfig.DB.Unscoped().Where("deleted_at IS NOT NULL").Order("created_at desc").Find(&corsBlacklist)
|
||
dbConfig.DB.Unscoped().Where("deleted_at IS NOT NULL").Order("created_at desc").Find(&rateLimits)
|
||
} else {
|
||
dbConfig.DB.Order("created_at desc").Find(&corsWhitelist)
|
||
dbConfig.DB.Order("created_at desc").Find(&corsBlacklist)
|
||
dbConfig.DB.Order("created_at desc").Find(&rateLimits)
|
||
}
|
||
|
||
// Check if we're editing an existing entry (via query params)
|
||
editWhitelistID := c.Query("edit_whitelist", "")
|
||
var editWhitelist models.CorsWhitelist
|
||
if editWhitelistID != "" {
|
||
if err := dbConfig.DB.First(&editWhitelist, editWhitelistID).Error; err == nil {
|
||
// found, will pass to template
|
||
}
|
||
}
|
||
|
||
editBlacklistID := c.Query("edit_blacklist", "")
|
||
var editBlacklist models.CorsBlacklist
|
||
if editBlacklistID != "" {
|
||
if err := dbConfig.DB.First(&editBlacklist, editBlacklistID).Error; err == nil {
|
||
}
|
||
}
|
||
|
||
editRateLimitID := c.Query("edit_ratelimit", "")
|
||
var editRateLimit models.RateLimitSetting
|
||
if editRateLimitID != "" {
|
||
if err := dbConfig.DB.First(&editRateLimit, editRateLimitID).Error; err == nil {
|
||
}
|
||
}
|
||
|
||
data := fiber.Map{
|
||
"Setting": setting,
|
||
"Heroes": heroes,
|
||
"ShowDeleted": showDeleted,
|
||
"CorsWhitelist": corsWhitelist,
|
||
"CorsBlacklist": corsBlacklist,
|
||
"RateLimits": rateLimits,
|
||
"EditWhitelist": editWhitelist,
|
||
"EditBlacklist": editBlacklist,
|
||
"EditRateLimit": editRateLimit,
|
||
}
|
||
|
||
if c.Get("HX-Request") == "true" {
|
||
return c.Render("admin/partials/settings", data)
|
||
}
|
||
return c.Render("admin/partials/settings", data, "admin/layout")
|
||
}
|
||
|
||
// AdminSettingsPost handles the settings form submission
|
||
func AdminSettingsPost(c fiber.Ctx) error {
|
||
configs.Logger.Info(
|
||
"AdminSettingsPost called",
|
||
zap.String("method", c.Method()),
|
||
zap.String("path", c.Path()),
|
||
zap.String("content_type", c.Get("Content-Type")),
|
||
)
|
||
|
||
if form, err := c.MultipartForm(); err == nil && form != nil {
|
||
configs.Logger.Info(
|
||
"AdminSettingsPost multipart received",
|
||
zap.Int("w_logo_count", len(form.File["w_logo"])),
|
||
zap.Int("b_logo_count", len(form.File["b_logo"])),
|
||
)
|
||
} else {
|
||
configs.Logger.Warn("AdminSettingsPost multipart parse failed", zap.Error(err))
|
||
}
|
||
|
||
var setting models.Setting
|
||
// Fetch existing or create new
|
||
if err := dbConfig.DB.First(&setting).Error; err != nil {
|
||
// Create new if doesn't exist
|
||
setting = models.Setting{}
|
||
}
|
||
|
||
// Eski logo yollarını sakla; yeni dosya yüklenmezse bunları koruyacağız
|
||
oldWLogo := setting.WLogo
|
||
oldBLogo := setting.BLogo
|
||
|
||
// Parse form
|
||
if err := c.Bind().Body(&setting); err != nil {
|
||
return c.Status(fiber.StatusBadRequest).SendString("<div class='alert alert-danger'>Form verileri okunamadı.</div>")
|
||
}
|
||
|
||
// Handle checkboxes (boolean) manually if needed
|
||
isActive := c.FormValue("is_active") == "on"
|
||
setting.IsActive = isActive
|
||
|
||
// Handle Image Uploads for Settings
|
||
// 1. White Logo (w_logo)
|
||
wWidth, _ := strconv.Atoi(c.FormValue("w_width"))
|
||
wHeight, _ := strconv.Atoi(c.FormValue("w_height"))
|
||
wQuality, _ := strconv.Atoi(c.FormValue("w_quality"))
|
||
wFormat := c.FormValue("w_format")
|
||
|
||
wLogoPath, err := services.ProcessAndSaveImage(c, "w_logo", services.ImageOptions{
|
||
Width: wWidth,
|
||
Height: wHeight,
|
||
Quality: wQuality,
|
||
Format: wFormat,
|
||
Folder: "settings",
|
||
})
|
||
if err == nil && wLogoPath != "" {
|
||
setting.WLogo = wLogoPath
|
||
} else {
|
||
// Yeni dosya yoksa/başarısızsa eski logoyu koru
|
||
setting.WLogo = oldWLogo
|
||
}
|
||
|
||
// 2. Black Logo (b_logo)
|
||
bWidth, _ := strconv.Atoi(c.FormValue("b_width"))
|
||
bHeight, _ := strconv.Atoi(c.FormValue("b_height"))
|
||
bQuality, _ := strconv.Atoi(c.FormValue("b_quality"))
|
||
bFormat := c.FormValue("b_format")
|
||
|
||
bLogoPath, err := services.ProcessAndSaveImage(c, "b_logo", services.ImageOptions{
|
||
Width: bWidth,
|
||
Height: bHeight,
|
||
Quality: bQuality,
|
||
Format: bFormat,
|
||
Folder: "settings",
|
||
})
|
||
if err == nil && bLogoPath != "" {
|
||
setting.BLogo = bLogoPath
|
||
} else {
|
||
// Yeni dosya yoksa/başarısızsa eski logoyu koru
|
||
setting.BLogo = oldBLogo
|
||
}
|
||
|
||
if err := dbConfig.DB.Save(&setting).Error; err != nil {
|
||
return c.Status(fiber.StatusInternalServerError).SendString("<div class='alert alert-danger'>Ayarlar kaydedilirken hata oluştu.</div>")
|
||
}
|
||
|
||
return c.Redirect().To("/admin/content/settings?success=Ayarlar+kaydedildi")
|
||
}
|
||
|
||
// AdminCorsWhitelistCreate handles creating a new CORS whitelist entry
|
||
func AdminCorsWhitelistCreate(c fiber.Ctx) error {
|
||
origin := c.FormValue("origin")
|
||
description := c.FormValue("description")
|
||
isActive := c.FormValue("is_active") == "on"
|
||
|
||
if origin == "" {
|
||
return c.Redirect().To("/admin/content/settings?error=Origin+gerekiyor")
|
||
}
|
||
|
||
entry := models.CorsWhitelist{
|
||
Origin: origin,
|
||
Description: description,
|
||
IsActive: isActive,
|
||
}
|
||
if err := dbConfig.DB.Create(&entry).Error; err != nil {
|
||
return c.Redirect().To("/admin/content/settings?error=Oluşturma+başarısız")
|
||
}
|
||
return c.Redirect().To("/admin/content/settings?success=Whitelist+eklendi")
|
||
}
|
||
|
||
// AdminCorsWhitelistDelete soft-deletes a whitelist entry
|
||
func AdminCorsWhitelistDelete(c fiber.Ctx) error {
|
||
id := c.Params("id")
|
||
if err := dbConfig.DB.Delete(&models.CorsWhitelist{}, id).Error; err != nil {
|
||
return c.Redirect().To("/admin/content/settings?error=Silme+başarısız")
|
||
}
|
||
return c.Redirect().To("/admin/content/settings?success=Whitelist+silindi")
|
||
}
|
||
|
||
// AdminCorsBlacklistCreate handles creating a new CORS blacklist entry
|
||
func AdminCorsBlacklistCreate(c fiber.Ctx) error {
|
||
origin := c.FormValue("origin")
|
||
reason := c.FormValue("reason")
|
||
isActive := c.FormValue("is_active") == "on"
|
||
|
||
if origin == "" {
|
||
return c.Redirect().To("/admin/content/settings?error=Origin+gerekiyor")
|
||
}
|
||
|
||
entry := models.CorsBlacklist{
|
||
Origin: origin,
|
||
Reason: reason,
|
||
IsActive: isActive,
|
||
}
|
||
if err := dbConfig.DB.Create(&entry).Error; err != nil {
|
||
return c.Redirect().To("/admin/content/settings?error=Oluşturma+başarısız")
|
||
}
|
||
return c.Redirect().To("/admin/content/settings?success=Blacklist+eklendi")
|
||
}
|
||
|
||
// AdminCorsBlacklistDelete soft-deletes a blacklist entry
|
||
func AdminCorsBlacklistDelete(c fiber.Ctx) error {
|
||
id := c.Params("id")
|
||
if err := dbConfig.DB.Delete(&models.CorsBlacklist{}, id).Error; err != nil {
|
||
return c.Redirect().To("/admin/content/settings?error=Silme+başarısız")
|
||
}
|
||
return c.Redirect().To("/admin/content/settings?success=Blacklist+silindi")
|
||
}
|
||
|
||
// AdminRateLimitCreate creates a rate limit setting
|
||
func AdminRateLimitCreate(c fiber.Ctx) error {
|
||
name := c.FormValue("name")
|
||
description := c.FormValue("description")
|
||
maxReq := c.FormValue("max_requests")
|
||
window := c.FormValue("window_seconds")
|
||
isActive := c.FormValue("is_active") == "on"
|
||
|
||
if name == "" || maxReq == "" || window == "" {
|
||
return c.Redirect().To("/admin/content/settings?error=Eksik+alan")
|
||
}
|
||
maxI, _ := strconv.ParseInt(maxReq, 10, 64)
|
||
winI, _ := strconv.Atoi(window)
|
||
|
||
rl := models.RateLimitSetting{
|
||
Name: name,
|
||
Description: description,
|
||
MaxRequests: maxI,
|
||
WindowSeconds: winI,
|
||
IsActive: isActive,
|
||
}
|
||
if err := dbConfig.DB.Create(&rl).Error; err != nil {
|
||
return c.Redirect().To("/admin/content/settings?error=Oluşturma+başarısız")
|
||
}
|
||
return c.Redirect().To("/admin/content/settings?success=Rate+limit+eklendi")
|
||
}
|
||
|
||
// AdminRateLimitDelete deletes a rate limit setting
|
||
func AdminRateLimitDelete(c fiber.Ctx) error {
|
||
id := c.Params("id")
|
||
if err := dbConfig.DB.Delete(&models.RateLimitSetting{}, id).Error; err != nil {
|
||
return c.Redirect().To("/admin/content/settings?error=Silme+başarısız")
|
||
}
|
||
return c.Redirect().To("/admin/content/settings?success=Rate+limit+silindi")
|
||
}
|
||
|
||
// AdminCorsWhitelistRestore restores a soft-deleted whitelist entry
|
||
func AdminCorsWhitelistRestore(c fiber.Ctx) error {
|
||
id := c.Params("id")
|
||
if err := dbConfig.DB.Unscoped().Model(&models.CorsWhitelist{}).Where("id = ?", id).Update("deleted_at", nil).Error; err != nil {
|
||
return c.Redirect().To("/admin/content/settings?error=Geri+yükleme+başarısız")
|
||
}
|
||
return c.Redirect().To("/admin/content/settings?success=Whitelist+geri+yüklendi")
|
||
}
|
||
|
||
// AdminCorsBlacklistRestore restores a soft-deleted blacklist entry
|
||
func AdminCorsBlacklistRestore(c fiber.Ctx) error {
|
||
id := c.Params("id")
|
||
if err := dbConfig.DB.Unscoped().Model(&models.CorsBlacklist{}).Where("id = ?", id).Update("deleted_at", nil).Error; err != nil {
|
||
return c.Redirect().To("/admin/content/settings?error=Geri+yükleme+başarısız")
|
||
}
|
||
return c.Redirect().To("/admin/content/settings?success=Blacklist+geri+yüklendi")
|
||
}
|
||
|
||
// AdminRateLimitRestore restores a soft-deleted rate limit entry
|
||
func AdminRateLimitRestore(c fiber.Ctx) error {
|
||
id := c.Params("id")
|
||
if err := dbConfig.DB.Unscoped().Model(&models.RateLimitSetting{}).Where("id = ?", id).Update("deleted_at", nil).Error; err != nil {
|
||
return c.Redirect().To("/admin/content/settings?error=Geri+yükleme+başarısız")
|
||
}
|
||
return c.Redirect().To("/admin/content/settings?success=Rate+limit+geri+yüklendi")
|
||
}
|
||
|
||
// AdminCorsWhitelistUpdate updates an existing whitelist entry
|
||
func AdminCorsWhitelistUpdate(c fiber.Ctx) error {
|
||
id := c.Params("id")
|
||
var entry models.CorsWhitelist
|
||
if err := dbConfig.DB.First(&entry, id).Error; err != nil {
|
||
return c.Redirect().To("/admin/content/settings?error=Whitelist+bulunamadı")
|
||
}
|
||
|
||
entry.Origin = c.FormValue("origin")
|
||
entry.Description = c.FormValue("description")
|
||
entry.IsActive = c.FormValue("is_active") == "on"
|
||
|
||
if err := dbConfig.DB.Save(&entry).Error; err != nil {
|
||
return c.Redirect().To("/admin/content/settings?error=Güncelleme+başarısız")
|
||
}
|
||
return c.Redirect().To("/admin/content/settings?success=Whitelist+güncellendi")
|
||
}
|
||
|
||
// AdminCorsBlacklistUpdate updates an existing blacklist entry
|
||
func AdminCorsBlacklistUpdate(c fiber.Ctx) error {
|
||
id := c.Params("id")
|
||
var entry models.CorsBlacklist
|
||
if err := dbConfig.DB.First(&entry, id).Error; err != nil {
|
||
return c.Redirect().To("/admin/content/settings?error=Blacklist+bulunamadı")
|
||
}
|
||
|
||
entry.Origin = c.FormValue("origin")
|
||
entry.Reason = c.FormValue("reason")
|
||
entry.IsActive = c.FormValue("is_active") == "on"
|
||
|
||
if err := dbConfig.DB.Save(&entry).Error; err != nil {
|
||
return c.Redirect().To("/admin/content/settings?error=Güncelleme+başarısız")
|
||
}
|
||
return c.Redirect().To("/admin/content/settings?success=Blacklist+güncellendi")
|
||
}
|
||
|
||
// AdminRateLimitUpdate updates an existing rate limit entry
|
||
func AdminRateLimitUpdate(c fiber.Ctx) error {
|
||
id := c.Params("id")
|
||
var rl models.RateLimitSetting
|
||
if err := dbConfig.DB.First(&rl, id).Error; err != nil {
|
||
return c.Redirect().To("/admin/content/settings?error=Rate+limit+bulunamadı")
|
||
}
|
||
|
||
rl.Name = c.FormValue("name")
|
||
rl.Description = c.FormValue("description")
|
||
maxReq, _ := strconv.ParseInt(c.FormValue("max_requests"), 10, 64)
|
||
win, _ := strconv.Atoi(c.FormValue("window_seconds"))
|
||
rl.MaxRequests = maxReq
|
||
rl.WindowSeconds = win
|
||
rl.IsActive = c.FormValue("is_active") == "on"
|
||
|
||
if err := dbConfig.DB.Save(&rl).Error; err != nil {
|
||
return c.Redirect().To("/admin/content/settings?error=Güncelleme+başarısız")
|
||
}
|
||
return c.Redirect().To("/admin/content/settings?success=Rate+limit+güncellendi")
|
||
}
|
||
|
||
// --- Hero Management ---
|
||
|
||
// AdminHeroNew renders the create hero form
|
||
func AdminHeroNew(c fiber.Ctx) error {
|
||
return c.Render("admin/pages/hero_form", fiber.Map{
|
||
"IsEdit": false,
|
||
}, "admin/layout")
|
||
}
|
||
|
||
// AdminHeroCreate handles hero creation
|
||
func AdminHeroCreate(c fiber.Ctx) error {
|
||
hero := new(models.Hero)
|
||
if err := c.Bind().Body(hero); err != nil {
|
||
return c.Status(fiber.StatusBadRequest).SendString("Geçersiz veri")
|
||
}
|
||
|
||
// Checkbox handling
|
||
isActive := c.FormValue("is_active") == "on"
|
||
hero.IsActive = isActive
|
||
|
||
// Image Upload
|
||
width, _ := strconv.Atoi(c.FormValue("width"))
|
||
height, _ := strconv.Atoi(c.FormValue("height"))
|
||
quality, _ := strconv.Atoi(c.FormValue("quality"))
|
||
format := c.FormValue("format")
|
||
|
||
imagePath, err := services.ProcessAndSaveImage(c, "image", services.ImageOptions{
|
||
Width: width,
|
||
Height: height,
|
||
Quality: quality,
|
||
Format: format,
|
||
Folder: "heroes",
|
||
})
|
||
if err != nil {
|
||
return c.Status(fiber.StatusInternalServerError).SendString("Resim yükleme hatası: " + err.Error())
|
||
}
|
||
// If image uploaded, set it. For create, it's usually required or optimal.
|
||
if imagePath != "" {
|
||
hero.Image = imagePath
|
||
}
|
||
|
||
if err := dbConfig.DB.Create(hero).Error; err != nil {
|
||
return c.Status(fiber.StatusInternalServerError).SendString("Oluşturma hatası: " + err.Error())
|
||
}
|
||
|
||
return c.Redirect().To("/admin/content/settings?success=Banner+oluşturuldu")
|
||
}
|
||
|
||
// AdminHeroEdit renders the edit hero form
|
||
func AdminHeroEdit(c fiber.Ctx) error {
|
||
id := c.Params("id")
|
||
var hero models.Hero
|
||
if err := dbConfig.DB.First(&hero, id).Error; err != nil {
|
||
return c.Status(fiber.StatusNotFound).SendString("Banner bulunamadı")
|
||
}
|
||
|
||
return c.Render("admin/pages/hero_form", fiber.Map{
|
||
"IsEdit": true,
|
||
"Hero": hero,
|
||
}, "admin/layout")
|
||
}
|
||
|
||
// AdminHeroUpdate handles hero update
|
||
func AdminHeroUpdate(c fiber.Ctx) error {
|
||
id := c.Params("id")
|
||
var hero models.Hero
|
||
if err := dbConfig.DB.First(&hero, id).Error; err != nil {
|
||
return c.Status(fiber.StatusNotFound).SendString("Banner bulunamadı")
|
||
}
|
||
|
||
// Text and numeric fields: update from form values without wiping existing data if empty
|
||
if v := c.FormValue("color"); v != "" {
|
||
hero.Color = v
|
||
}
|
||
if v := c.FormValue("title"); v != "" {
|
||
hero.Title = v
|
||
}
|
||
if v := c.FormValue("text1"); v != "" {
|
||
hero.Text1 = v
|
||
}
|
||
if v := c.FormValue("text2"); v != "" {
|
||
hero.Text2 = v
|
||
}
|
||
if v := c.FormValue("text4"); v != "" {
|
||
hero.Text4 = v
|
||
}
|
||
if v := c.FormValue("text5"); v != "" {
|
||
hero.Text5 = v
|
||
}
|
||
|
||
// Checkbox handling
|
||
isActive := c.FormValue("is_active") == "on"
|
||
hero.IsActive = isActive
|
||
|
||
// Image Upload (Update if new file provided)
|
||
// Width/height/quality/format: only override if provided, otherwise keep existing values
|
||
var width = hero.Width
|
||
var height = hero.Height
|
||
var quality = hero.Quality
|
||
var format = hero.Format
|
||
|
||
if v := c.FormValue("width"); v != "" {
|
||
if parsed, err := strconv.Atoi(v); err == nil {
|
||
width = parsed
|
||
hero.Width = parsed
|
||
}
|
||
}
|
||
if v := c.FormValue("height"); v != "" {
|
||
if parsed, err := strconv.Atoi(v); err == nil {
|
||
height = parsed
|
||
hero.Height = parsed
|
||
}
|
||
}
|
||
if v := c.FormValue("quality"); v != "" {
|
||
if parsed, err := strconv.Atoi(v); err == nil {
|
||
quality = parsed
|
||
hero.Quality = parsed
|
||
}
|
||
}
|
||
if v := c.FormValue("format"); v != "" {
|
||
format = v
|
||
hero.Format = v
|
||
}
|
||
|
||
imagePath, err := services.ProcessAndSaveImage(c, "image", services.ImageOptions{
|
||
Width: width,
|
||
Height: height,
|
||
Quality: quality,
|
||
Format: format,
|
||
Folder: "heroes",
|
||
})
|
||
if err == nil && imagePath != "" {
|
||
hero.Image = imagePath
|
||
}
|
||
|
||
if err := dbConfig.DB.Save(&hero).Error; err != nil {
|
||
return c.Status(fiber.StatusInternalServerError).SendString("Güncelleme hatası")
|
||
}
|
||
|
||
return c.Redirect().To("/admin/content/settings?success=Banner+güncellendi")
|
||
}
|
||
|
||
// AdminHeroDelete handles hero soft delete
|
||
func AdminHeroDelete(c fiber.Ctx) error {
|
||
id := c.Params("id")
|
||
if err := dbConfig.DB.Delete(&models.Hero{}, id).Error; err != nil {
|
||
return c.Status(fiber.StatusInternalServerError).SendString("Silme hatası")
|
||
}
|
||
return c.Redirect().To("/admin/content/settings?success=Banner+silindi")
|
||
}
|
||
|
||
// AdminHeroRestore restores a soft-deleted hero
|
||
func AdminHeroRestore(c fiber.Ctx) error {
|
||
id := c.Params("id")
|
||
if err := dbConfig.DB.Unscoped().Model(&models.Hero{}).Where("id = ?", id).Update("deleted_at", nil).Error; err != nil {
|
||
return c.Status(fiber.StatusInternalServerError).SendString("Geri yükleme hatası")
|
||
}
|
||
return c.Redirect().To("/admin/content/settings?deleted=true&success=Banner+geri+yüklendi")
|
||
}
|
||
|
||
// --- Category (Blog) Management for Admin ---
|
||
|
||
// AdminContentCategories renders category list (HTMX-aware)
|
||
func AdminContentCategories(c fiber.Ctx) error {
|
||
page, _ := strconv.Atoi(c.Query("page", "1"))
|
||
limit := 20
|
||
offset := (page - 1) * limit
|
||
search := c.Query("search", "")
|
||
showDeleted := c.Query("deleted") == "true"
|
||
|
||
var categories []models.Category
|
||
var total int64
|
||
|
||
query := dbConfig.DB.Model(&models.Category{})
|
||
if showDeleted {
|
||
query = query.Unscoped().Where("deleted_at IS NOT NULL")
|
||
}
|
||
if search != "" {
|
||
query = query.Where("title LIKE ? OR slug LIKE ?", "%"+search+"%", "%"+search+"%")
|
||
}
|
||
query.Count(&total)
|
||
// preload Parent so templates can display parent title
|
||
query.Preload("Parent").Order("created_at desc").Limit(limit).Offset(offset).Find(&categories)
|
||
|
||
totalPages := int(math.Ceil(float64(total) / float64(limit)))
|
||
|
||
data := fiber.Map{
|
||
"Categories": categories,
|
||
"Page": page,
|
||
"TotalPages": totalPages,
|
||
"NextPage": page + 1,
|
||
"PrevPage": page - 1,
|
||
"Search": search,
|
||
"ShowDeleted": showDeleted,
|
||
}
|
||
|
||
if c.Get("HX-Request") == "true" {
|
||
return c.Render("admin/partials/categories", data)
|
||
}
|
||
return c.Render("admin/partials/categories", data, "admin/layout")
|
||
}
|
||
|
||
// AdminCategoryNew renders create form
|
||
func AdminCategoryNew(c fiber.Ctx) error {
|
||
// load possible parents
|
||
var parents []models.Category
|
||
dbConfig.DB.Order("title asc").Find(&parents)
|
||
return c.Render("admin/pages/category_form", fiber.Map{
|
||
"IsEdit": false,
|
||
"Parents": parents,
|
||
}, "admin/layout")
|
||
}
|
||
|
||
// AdminCategoryCreate handles creation
|
||
func AdminCategoryCreate(c fiber.Ctx) error {
|
||
cat := models.Category{}
|
||
cat.Title = c.FormValue("title")
|
||
|
||
// Generate or sanitize slug
|
||
rawSlug := c.FormValue("slug")
|
||
if rawSlug == "" {
|
||
rawSlug = utils.Slugify(cat.Title)
|
||
} else {
|
||
rawSlug = utils.Slugify(rawSlug)
|
||
}
|
||
// ensure uniqueness
|
||
attempt := rawSlug
|
||
i := 1
|
||
|
||
for {
|
||
var existing models.Category
|
||
if err := dbConfig.DB.Unscoped().Where("slug = ?", attempt).First(&existing).Error; err != nil {
|
||
break
|
||
}
|
||
attempt = fmt.Sprintf("%s-%d", rawSlug, i)
|
||
i++
|
||
}
|
||
cat.Slug = attempt
|
||
|
||
if cat.Title == "" || cat.Slug == "" {
|
||
return c.Redirect().To("/admin/content/categories?error=Başlık+ve+slug+gerekli")
|
||
}
|
||
if err := dbConfig.DB.Create(&cat).Error; err != nil {
|
||
return c.Redirect().To("/admin/content/categories?error=Oluşturma+başarısız")
|
||
}
|
||
return c.Redirect().To("/admin/content/categories?success=Kategori+eklendi")
|
||
}
|
||
|
||
// AdminCategoryEdit renders edit form
|
||
func AdminCategoryEdit(c fiber.Ctx) error {
|
||
id := c.Params("id")
|
||
var cat models.Category
|
||
if err := dbConfig.DB.First(&cat, id).Error; err != nil {
|
||
return c.Status(fiber.StatusNotFound).SendString("Kategori bulunamadı")
|
||
}
|
||
var parents []models.Category
|
||
dbConfig.DB.Where("id != ?", cat.ID).Order("title asc").Find(&parents)
|
||
// pass parent id value for easier template comparison
|
||
var parentID uint = 0
|
||
if cat.ParentID != nil {
|
||
parentID = *cat.ParentID
|
||
}
|
||
return c.Render("admin/pages/category_form", fiber.Map{
|
||
"IsEdit": true,
|
||
"Category": cat,
|
||
"Parents": parents,
|
||
"ParentID": parentID,
|
||
}, "admin/layout")
|
||
}
|
||
|
||
// AdminCategoryUpdate handles update
|
||
func AdminCategoryUpdate(c fiber.Ctx) error {
|
||
id := c.Params("id")
|
||
var cat models.Category
|
||
if err := dbConfig.DB.First(&cat, id).Error; err != nil {
|
||
return c.Redirect().To("/admin/content/categories?error=Kategori+bulunamadı")
|
||
}
|
||
cat.Title = c.FormValue("title")
|
||
// sanitize/generate slug; allow keeping unique (exclude current record)
|
||
rawSlug := c.FormValue("slug")
|
||
if rawSlug == "" {
|
||
rawSlug = utils.Slugify(cat.Title)
|
||
} else {
|
||
rawSlug = utils.Slugify(rawSlug)
|
||
}
|
||
attempt := rawSlug
|
||
i := 1
|
||
for {
|
||
var existing models.Category
|
||
if err := dbConfig.DB.Unscoped().Where("slug = ? AND id != ?", attempt, cat.ID).First(&existing).Error; err != nil {
|
||
break
|
||
}
|
||
attempt = fmt.Sprintf("%s-%d", rawSlug, i)
|
||
i++
|
||
}
|
||
cat.Slug = attempt
|
||
cat.Description = c.FormValue("description")
|
||
if pid := c.FormValue("parent_id"); pid != "" {
|
||
if v, err := strconv.ParseUint(pid, 10, 64); err == nil {
|
||
p := uint(v)
|
||
cat.ParentID = &p
|
||
}
|
||
} else {
|
||
cat.ParentID = nil
|
||
}
|
||
|
||
if err := dbConfig.DB.Save(&cat).Error; err != nil {
|
||
return c.Redirect().To("/admin/content/categories?error=Güncelleme+başarısız")
|
||
}
|
||
return c.Redirect().To("/admin/content/categories?success=Kategori+güncellendi")
|
||
}
|
||
|
||
// AdminCategoryDelete soft-delete
|
||
func AdminCategoryDelete(c fiber.Ctx) error {
|
||
id := c.Params("id")
|
||
if err := dbConfig.DB.Delete(&models.Category{}, id).Error; err != nil {
|
||
return c.Redirect().To("/admin/content/categories?error=Silme+başarısız")
|
||
}
|
||
return c.Redirect().To("/admin/content/categories?success=Kategori+silindi")
|
||
}
|
||
|
||
// AdminCategoryRestore restores soft-deleted
|
||
func AdminCategoryRestore(c fiber.Ctx) error {
|
||
id := c.Params("id")
|
||
if err := dbConfig.DB.Unscoped().Model(&models.Category{}).Where("id = ?", id).Update("deleted_at", nil).Error; err != nil {
|
||
return c.Redirect().To("/admin/content/categories?error=Geri+yükleme+başarısız")
|
||
}
|
||
return c.Redirect().To("/admin/content/categories?deleted=true&success=Kategori+geri+yüklendi")
|
||
}
|
||
|
||
// AdminContentCategoryViews lists category view records
|
||
func AdminContentCategoryViews(c fiber.Ctx) error {
|
||
page, _ := strconv.Atoi(c.Query("page", "1"))
|
||
limit := 20
|
||
offset := (page - 1) * limit
|
||
search := c.Query("search", "")
|
||
showDeleted := c.Query("deleted") == "true"
|
||
|
||
var views []models.CategoryView
|
||
var total int64
|
||
|
||
query := dbConfig.DB.Model(&models.CategoryView{})
|
||
if showDeleted {
|
||
query = query.Unscoped().Where("deleted_at IS NOT NULL")
|
||
}
|
||
if search != "" {
|
||
query = query.Where("ip_address LIKE ?", "%"+search+"%")
|
||
}
|
||
query.Count(&total)
|
||
query.Order("created_at desc").Limit(limit).Offset(offset).Find(&views)
|
||
|
||
// build map of category titles
|
||
catIDs := make([]uint, 0)
|
||
for _, v := range views {
|
||
if v.CategoryID != 0 {
|
||
catIDs = append(catIDs, v.CategoryID)
|
||
}
|
||
}
|
||
var cats []models.Category
|
||
if len(catIDs) > 0 {
|
||
dbConfig.DB.Where("id IN ?", catIDs).Find(&cats)
|
||
}
|
||
catMap := make(map[uint]string)
|
||
for _, c := range cats {
|
||
catMap[c.ID] = c.Title
|
||
}
|
||
|
||
totalPages := int(math.Ceil(float64(total) / float64(limit)))
|
||
|
||
data := fiber.Map{
|
||
"Views": views,
|
||
"CatMap": catMap,
|
||
"Page": page,
|
||
"TotalPages": totalPages,
|
||
"NextPage": page + 1,
|
||
"PrevPage": page - 1,
|
||
"Search": search,
|
||
"ShowDeleted": showDeleted,
|
||
}
|
||
|
||
if c.Get("HX-Request") == "true" {
|
||
return c.Render("admin/partials/category_views", data)
|
||
}
|
||
return c.Render("admin/partials/category_views", data, "admin/layout")
|
||
}
|
||
|
||
// AdminCategoryViewDelete soft-delete a view record
|
||
func AdminCategoryViewDelete(c fiber.Ctx) error {
|
||
id := c.Params("id")
|
||
if err := dbConfig.DB.Delete(&models.CategoryView{}, id).Error; err != nil {
|
||
return c.Redirect().To("/admin/content/category-views?error=Silme+başarısız")
|
||
}
|
||
return c.Redirect().To("/admin/content/category-views?success=Kayıt+silindi")
|
||
}
|
||
|
||
// AdminCategoryViewRestore restores soft-deleted view record
|
||
func AdminCategoryViewRestore(c fiber.Ctx) error {
|
||
id := c.Params("id")
|
||
if err := dbConfig.DB.Unscoped().Model(&models.CategoryView{}).Where("id = ?", id).Update("deleted_at", nil).Error; err != nil {
|
||
return c.Redirect().To("/admin/content/category-views?error=Geri+yükleme+başarısız")
|
||
}
|
||
return c.Redirect().To("/admin/content/category-views?deleted=true&success=Kayıt+geri+yüklendi")
|
||
}
|
||
|
||
// AdminContentComments lists comments
|
||
func AdminContentComments(c fiber.Ctx) error {
|
||
page, _ := strconv.Atoi(c.Query("page", "1"))
|
||
limit := 20
|
||
offset := (page - 1) * limit
|
||
search := c.Query("search", "")
|
||
showDeleted := c.Query("deleted") == "true"
|
||
|
||
var comments []models.Comment
|
||
var total int64
|
||
|
||
query := dbConfig.DB.Model(&models.Comment{})
|
||
if showDeleted {
|
||
query = query.Unscoped().Where("deleted_at IS NOT NULL")
|
||
}
|
||
if search != "" {
|
||
query = query.Where("body LIKE ?", "%"+search+"%")
|
||
}
|
||
query.Count(&total)
|
||
query.Order("created_at desc").Limit(limit).Offset(offset).Find(&comments)
|
||
|
||
totalPages := int(math.Ceil(float64(total) / float64(limit)))
|
||
|
||
data := fiber.Map{
|
||
"Comments": comments,
|
||
"Page": page,
|
||
"TotalPages": totalPages,
|
||
"NextPage": page + 1,
|
||
"PrevPage": page - 1,
|
||
"Search": search,
|
||
"ShowDeleted": showDeleted,
|
||
}
|
||
|
||
if c.Get("HX-Request") == "true" {
|
||
return c.Render("admin/partials/comments", data)
|
||
}
|
||
return c.Render("admin/partials/comments", data, "admin/layout")
|
||
}
|
||
|
||
// AdminCommentEdit renders edit form for a comment
|
||
func AdminCommentEdit(c fiber.Ctx) error {
|
||
id := c.Params("id")
|
||
var comment models.Comment
|
||
if err := dbConfig.DB.First(&comment, id).Error; err != nil {
|
||
return c.Status(fiber.StatusNotFound).SendString("Yorum bulunamadı")
|
||
}
|
||
return c.Render("admin/pages/comment_form", fiber.Map{"IsEdit": true, "Comment": comment}, "admin/layout")
|
||
}
|
||
|
||
// AdminCommentUpdate updates a comment
|
||
func AdminCommentUpdate(c fiber.Ctx) error {
|
||
id := c.Params("id")
|
||
var comment models.Comment
|
||
if err := dbConfig.DB.First(&comment, id).Error; err != nil {
|
||
return c.Redirect().To("/admin/content/comments?error=Yorum+bulunamadı")
|
||
}
|
||
comment.Body = c.FormValue("body")
|
||
if err := dbConfig.DB.Save(&comment).Error; err != nil {
|
||
return c.Redirect().To("/admin/content/comments?error=Güncelleme+başarısız")
|
||
}
|
||
return c.Redirect().To("/admin/content/comments?success=Yorum+güncellendi")
|
||
}
|
||
|
||
// AdminCommentDelete soft-delete
|
||
func AdminCommentDelete(c fiber.Ctx) error {
|
||
id := c.Params("id")
|
||
if err := dbConfig.DB.Delete(&models.Comment{}, id).Error; err != nil {
|
||
return c.Redirect().To("/admin/content/comments?error=Silme+başarısız")
|
||
}
|
||
return c.Redirect().To("/admin/content/comments?success=Yorum+silindi")
|
||
}
|
||
|
||
// AdminCommentRestore restores a soft-deleted comment
|
||
func AdminCommentRestore(c fiber.Ctx) error {
|
||
id := c.Params("id")
|
||
if err := dbConfig.DB.Unscoped().Model(&models.Comment{}).Where("id = ?", id).Update("deleted_at", nil).Error; err != nil {
|
||
return c.Redirect().To("/admin/content/comments?error=Geri+yükleme+başarısız")
|
||
}
|
||
return c.Redirect().To("/admin/content/comments?deleted=true&success=Yorum+geri+yüklendi")
|
||
}
|
||
|
||
// --- Tag Management (Admin) ---
|
||
|
||
// AdminContentTags renders tag list (HTMX-aware)
|
||
func AdminContentTags(c fiber.Ctx) error {
|
||
page, _ := strconv.Atoi(c.Query("page", "1"))
|
||
limit := 20
|
||
offset := (page - 1) * limit
|
||
search := c.Query("search", "")
|
||
showDeleted := c.Query("deleted") == "true"
|
||
|
||
var tags []models.Tag
|
||
var total int64
|
||
|
||
query := dbConfig.DB.Model(&models.Tag{})
|
||
if showDeleted {
|
||
query = query.Unscoped().Where("deleted_at IS NOT NULL")
|
||
}
|
||
if search != "" {
|
||
query = query.Where("name LIKE ?", "%"+search+"%")
|
||
}
|
||
query.Count(&total)
|
||
query.Order("created_at desc").Limit(limit).Offset(offset).Find(&tags)
|
||
|
||
totalPages := int(math.Ceil(float64(total) / float64(limit)))
|
||
|
||
data := fiber.Map{
|
||
"Tags": tags,
|
||
"Page": page,
|
||
"TotalPages": totalPages,
|
||
"NextPage": page + 1,
|
||
"PrevPage": page - 1,
|
||
"Search": search,
|
||
"ShowDeleted": showDeleted,
|
||
}
|
||
|
||
if c.Get("HX-Request") == "true" {
|
||
return c.Render("admin/partials/tags", data)
|
||
}
|
||
return c.Render("admin/partials/tags", data, "admin/layout")
|
||
}
|
||
|
||
// AdminTagNew renders create form
|
||
func AdminTagNew(c fiber.Ctx) error {
|
||
return c.Render("admin/pages/tag_form", fiber.Map{"IsEdit": false}, "admin/layout")
|
||
}
|
||
|
||
// AdminTagCreate handles tag creation
|
||
func AdminTagCreate(c fiber.Ctx) error {
|
||
name := c.FormValue("name")
|
||
if name == "" {
|
||
return c.Redirect().To("/admin/content/tags?error=İsim+gerekli")
|
||
}
|
||
tag := models.Tag{Name: name}
|
||
if err := dbConfig.DB.Create(&tag).Error; err != nil {
|
||
return c.Redirect().To("/admin/content/tags?error=Oluşturma+başarısız")
|
||
}
|
||
return c.Redirect().To("/admin/content/tags?success=Tag+eklendi")
|
||
}
|
||
|
||
// AdminTagEdit renders edit form
|
||
func AdminTagEdit(c fiber.Ctx) error {
|
||
id := c.Params("id")
|
||
var tag models.Tag
|
||
if err := dbConfig.DB.First(&tag, id).Error; err != nil {
|
||
return c.Status(fiber.StatusNotFound).SendString("Tag bulunamadı")
|
||
}
|
||
return c.Render("admin/pages/tag_form", fiber.Map{"IsEdit": true, "Tag": tag}, "admin/layout")
|
||
}
|
||
|
||
// AdminTagUpdate handles update
|
||
func AdminTagUpdate(c fiber.Ctx) error {
|
||
id := c.Params("id")
|
||
var tag models.Tag
|
||
if err := dbConfig.DB.First(&tag, id).Error; err != nil {
|
||
return c.Redirect().To("/admin/content/tags?error=Tag+bulunamadı")
|
||
}
|
||
tag.Name = c.FormValue("name")
|
||
if tag.Name == "" {
|
||
return c.Redirect().To("/admin/content/tags?error=İsim+gerekli")
|
||
}
|
||
if err := dbConfig.DB.Save(&tag).Error; err != nil {
|
||
return c.Redirect().To("/admin/content/tags?error=Güncelleme+başarısız")
|
||
}
|
||
return c.Redirect().To("/admin/content/tags?success=Tag+güncellendi")
|
||
}
|
||
|
||
// AdminTagDelete soft-delete
|
||
func AdminTagDelete(c fiber.Ctx) error {
|
||
id := c.Params("id")
|
||
if err := dbConfig.DB.Delete(&models.Tag{}, id).Error; err != nil {
|
||
return c.Redirect().To("/admin/content/tags?error=Silme+başarısız")
|
||
}
|
||
return c.Redirect().To("/admin/content/tags?success=Tag+silindi")
|
||
}
|
||
|
||
// AdminTagRestore restores soft-deleted tag
|
||
func AdminTagRestore(c fiber.Ctx) error {
|
||
id := c.Params("id")
|
||
if err := dbConfig.DB.Unscoped().Model(&models.Tag{}).Where("id = ?", id).Update("deleted_at", nil).Error; err != nil {
|
||
return c.Redirect().To("/admin/content/tags?error=Geri+yükleme+başarısız")
|
||
}
|
||
return c.Redirect().To("/admin/content/tags?deleted=true&success=Tag+geri+yüklendi")
|
||
}
|
||
|
||
// --- Post Management (Admin) ---
|
||
|
||
// AdminContentPosts renders posts list (HTMX-aware)
|
||
func AdminContentPosts(c fiber.Ctx) error {
|
||
page, _ := strconv.Atoi(c.Query("page", "1"))
|
||
limit := 20
|
||
offset := (page - 1) * limit
|
||
search := c.Query("search", "")
|
||
showDeleted := c.Query("deleted") == "true"
|
||
|
||
var posts []models.Post
|
||
var total int64
|
||
|
||
query := dbConfig.DB.Model(&models.Post{})
|
||
if showDeleted {
|
||
query = query.Unscoped().Where("deleted_at IS NOT NULL")
|
||
}
|
||
if search != "" {
|
||
query = query.Where("title LIKE ? OR slug LIKE ?", "%"+search+"%", "%"+search+"%")
|
||
}
|
||
query.Count(&total)
|
||
query.Preload("Categories").Preload("Tags").Order("created_at desc").Limit(limit).Offset(offset).Find(&posts)
|
||
|
||
// build first-image map for templates (posts.Images is stored as JSON array string)
|
||
imageMap := make(map[uint]string)
|
||
for _, p := range posts {
|
||
if p.Images != "" {
|
||
imgs := parseImagesField(p.Images)
|
||
if len(imgs) > 0 {
|
||
imageMap[p.ID] = imgs[0]
|
||
}
|
||
}
|
||
}
|
||
|
||
totalPages := int(math.Ceil(float64(total) / float64(limit)))
|
||
|
||
data := fiber.Map{
|
||
"Posts": posts,
|
||
"ImageMap": imageMap,
|
||
"Page": page,
|
||
"TotalPages": totalPages,
|
||
"NextPage": page + 1,
|
||
"PrevPage": page - 1,
|
||
"Search": search,
|
||
"ShowDeleted": showDeleted,
|
||
"Success": c.Query("success"),
|
||
"Error": c.Query("error"),
|
||
}
|
||
|
||
if c.Get("HX-Request") == "true" {
|
||
return c.Render("admin/partials/posts", data)
|
||
}
|
||
return c.Render("admin/partials/posts", data, "admin/layout")
|
||
}
|
||
|
||
// AdminPostNew renders create form
|
||
func AdminPostNew(c fiber.Ctx) error {
|
||
var cats []models.Category
|
||
var tags []models.Tag
|
||
dbConfig.DB.Order("title asc").Find(&cats)
|
||
dbConfig.DB.Order("name asc").Find(&tags)
|
||
return c.Render("admin/pages/post_form", fiber.Map{"IsEdit": false, "Categories": cats, "Tags": tags, "FirstImage": ""}, "admin/layout")
|
||
}
|
||
|
||
// AdminFetchImage downloads a remote image URL, processes it and returns saved path.
|
||
func AdminFetchImage(c fiber.Ctx) error {
|
||
var payload struct {
|
||
Url string `json:"url"`
|
||
Width int `json:"width"`
|
||
Height int `json:"height"`
|
||
Quality int `json:"quality"`
|
||
Format string `json:"format"`
|
||
}
|
||
// Parse JSON body (use json.Unmarshal on raw body for compatibility)
|
||
if err := json.Unmarshal(c.Body(), &payload); err != nil {
|
||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid payload"})
|
||
}
|
||
if payload.Url == "" {
|
||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "url required"})
|
||
}
|
||
|
||
// Fetch remote image with timeout and size limit
|
||
client := &http.Client{Timeout: 10 * time.Second}
|
||
resp, err := client.Get(payload.Url)
|
||
if err != nil {
|
||
return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"error": "failed to fetch url"})
|
||
}
|
||
defer resp.Body.Close()
|
||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||
return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"error": "failed to fetch url"})
|
||
}
|
||
ct := resp.Header.Get("Content-Type")
|
||
if ct == "" || !strings.HasPrefix(ct, "image/") {
|
||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "url is not an image"})
|
||
}
|
||
// limit read to 6MB
|
||
reader := io.LimitReader(resp.Body, 6*1024*1024)
|
||
data, err := io.ReadAll(reader)
|
||
if err != nil {
|
||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "could not read image"})
|
||
}
|
||
|
||
saved, err := services.ProcessAndSaveImageFromBytes(data, services.ImageOptions{
|
||
Width: payload.Width,
|
||
Height: payload.Height,
|
||
Quality: payload.Quality,
|
||
Format: payload.Format,
|
||
Folder: "posts",
|
||
})
|
||
if err != nil {
|
||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "could not process image"})
|
||
}
|
||
return c.JSON(fiber.Map{"url": saved})
|
||
}
|
||
|
||
// AdminPostCreate handles creation
|
||
func AdminPostCreate(c fiber.Ctx) error {
|
||
configs.Logger.Info(
|
||
"AdminPostCreate called",
|
||
zap.String("method", c.Method()),
|
||
zap.String("path", c.Path()),
|
||
zap.String("content_type", c.Get("Content-Type")),
|
||
)
|
||
|
||
title := c.FormValue("title")
|
||
if title == "" {
|
||
return c.Redirect().To("/admin/content/posts?error=Başlık+gerekli")
|
||
}
|
||
post := models.Post{Title: title}
|
||
post.Content = c.FormValue("content")
|
||
|
||
// Slug handling
|
||
rawSlug := c.FormValue("slug")
|
||
if rawSlug == "" {
|
||
rawSlug = utils.Slugify(post.Title)
|
||
} else {
|
||
rawSlug = utils.Slugify(rawSlug)
|
||
}
|
||
attempt := rawSlug
|
||
i := 1
|
||
for {
|
||
var existing models.Post
|
||
err := dbConfig.DB.Unscoped().Where("slug = ?", attempt).First(&existing).Error
|
||
if err != nil {
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
break
|
||
}
|
||
return c.Redirect().To("/admin/content/posts?error=DB+error")
|
||
}
|
||
attempt = fmt.Sprintf("%s-%d", rawSlug, i)
|
||
i++
|
||
}
|
||
post.Slug = attempt
|
||
|
||
// Categories
|
||
catIDs := c.FormValue("category_ids")
|
||
if catIDs != "" {
|
||
ids := parseIDsCSV(catIDs)
|
||
if len(ids) > 0 {
|
||
var cats []models.Category
|
||
dbConfig.DB.Find(&cats, ids)
|
||
post.Categories = cats
|
||
}
|
||
}
|
||
// Tags
|
||
tagIDs := c.FormValue("tag_ids")
|
||
if tagIDs != "" {
|
||
ids := parseIDsCSV(tagIDs)
|
||
if len(ids) > 0 {
|
||
var tags []models.Tag
|
||
dbConfig.DB.Find(&tags, ids)
|
||
post.Tags = tags
|
||
}
|
||
}
|
||
|
||
// Image processing using service
|
||
width, _ := strconv.Atoi(c.FormValue("width"))
|
||
height, _ := strconv.Atoi(c.FormValue("height"))
|
||
quality, _ := strconv.Atoi(c.FormValue("quality"))
|
||
format := c.FormValue("format")
|
||
|
||
imagePath, err := services.ProcessAndSaveImage(c, "image", services.ImageOptions{
|
||
Width: width,
|
||
Height: height,
|
||
Quality: quality,
|
||
Format: format,
|
||
Folder: "posts",
|
||
})
|
||
if err == nil && imagePath != "" {
|
||
// Ana görüntü
|
||
post.Images = imagePath
|
||
post.Width = width
|
||
post.Height = height
|
||
post.Quality = quality
|
||
if format != "" {
|
||
post.Format = format
|
||
}
|
||
|
||
// Orta boy (400x300)
|
||
midPath, errMid := services.ProcessAndSaveImage(c, "image", services.ImageOptions{
|
||
Width: 400,
|
||
Height: 300,
|
||
Quality: quality,
|
||
Format: format,
|
||
Folder: "posts",
|
||
})
|
||
if errMid != nil {
|
||
return c.Redirect().To("/admin/content/posts?error=Orta+boy+resim+oluşturulamadı")
|
||
}
|
||
post.ImagesMid = midPath
|
||
|
||
// Küçük boy (48x48)
|
||
minPath, errMin := services.ProcessAndSaveImage(c, "image", services.ImageOptions{
|
||
Width: 48,
|
||
Height: 48,
|
||
Quality: quality,
|
||
Format: format,
|
||
Folder: "posts",
|
||
})
|
||
if errMin != nil {
|
||
return c.Redirect().To("/admin/content/posts?error=Küçük+boy+resim+oluşturulamadı")
|
||
}
|
||
post.ImagesMin = minPath
|
||
} else if err != nil {
|
||
configs.Logger.Error("AdminPostCreate image upload failed", zap.Error(err))
|
||
return c.Redirect().To("/admin/content/posts?error=Image+API+key+gecersiz+veya+suresi+dolmus")
|
||
} else {
|
||
configs.Logger.Warn("AdminPostCreate image upload skipped or empty result")
|
||
}
|
||
|
||
if err := dbConfig.DB.Create(&post).Error; err != nil {
|
||
return c.Redirect().To("/admin/content/posts?error=Oluşturma+başarısız")
|
||
}
|
||
return c.Redirect().To("/admin/content/posts?success=Yazı+eklendi")
|
||
}
|
||
|
||
// AdminPostEdit renders edit form
|
||
func AdminPostEdit(c fiber.Ctx) error {
|
||
id := c.Params("id")
|
||
var post models.Post
|
||
if err := dbConfig.DB.Preload("Categories").Preload("Tags").First(&post, id).Error; err != nil {
|
||
return c.Status(fiber.StatusNotFound).SendString("Yazı bulunamadı")
|
||
}
|
||
var cats []models.Category
|
||
var tags []models.Tag
|
||
dbConfig.DB.Order("title asc").Find(&cats)
|
||
dbConfig.DB.Order("name asc").Find(&tags)
|
||
// extract first image if present (support plain string or JSON array)
|
||
firstImage := ""
|
||
if post.Images != "" {
|
||
imgs := parseImagesField(post.Images)
|
||
if len(imgs) > 0 {
|
||
firstImage = imgs[0]
|
||
}
|
||
}
|
||
return c.Render("admin/pages/post_form", fiber.Map{"IsEdit": true, "Post": post, "Categories": cats, "Tags": tags, "FirstImage": firstImage}, "admin/layout")
|
||
}
|
||
|
||
// AdminPostUpdate handles update
|
||
func AdminPostUpdate(c fiber.Ctx) error {
|
||
configs.Logger.Info(
|
||
"AdminPostUpdate called",
|
||
zap.String("method", c.Method()),
|
||
zap.String("path", c.Path()),
|
||
zap.String("content_type", c.Get("Content-Type")),
|
||
)
|
||
|
||
id := c.Params("id")
|
||
var post models.Post
|
||
if err := dbConfig.DB.Preload("Categories").Preload("Tags").First(&post, id).Error; err != nil {
|
||
return c.Redirect().To("/admin/content/posts?error=Yazı+bulunamadı")
|
||
}
|
||
title := c.FormValue("title")
|
||
if title != "" {
|
||
post.Title = title
|
||
rawSlug := c.FormValue("slug")
|
||
if rawSlug == "" {
|
||
rawSlug = utils.Slugify(post.Title)
|
||
} else {
|
||
rawSlug = utils.Slugify(rawSlug)
|
||
}
|
||
attempt := rawSlug
|
||
i := 1
|
||
for {
|
||
var existing models.Post
|
||
err := dbConfig.DB.Unscoped().Where("slug = ? AND id != ?", attempt, post.ID).First(&existing).Error
|
||
if err != nil {
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
break
|
||
}
|
||
return c.Redirect().To("/admin/content/posts?error=DB+error")
|
||
}
|
||
attempt = fmt.Sprintf("%s-%d", rawSlug, i)
|
||
i++
|
||
}
|
||
post.Slug = attempt
|
||
}
|
||
if content := c.FormValue("content"); content != "" {
|
||
post.Content = content
|
||
}
|
||
|
||
// Categories
|
||
if catIDs := c.FormValue("category_ids"); catIDs != "" {
|
||
ids := parseIDsCSV(catIDs)
|
||
if len(ids) > 0 {
|
||
var cats []models.Category
|
||
dbConfig.DB.Find(&cats, ids)
|
||
if err := dbConfig.DB.Model(&post).Association("Categories").Replace(&cats); err != nil {
|
||
return c.Redirect().To("/admin/content/posts?error=Kategori+güncelleme+başarısız")
|
||
}
|
||
}
|
||
}
|
||
// Tags
|
||
if tagIDs := c.FormValue("tag_ids"); tagIDs != "" {
|
||
ids := parseIDsCSV(tagIDs)
|
||
if len(ids) > 0 {
|
||
var tags []models.Tag
|
||
dbConfig.DB.Find(&tags, ids)
|
||
if err := dbConfig.DB.Model(&post).Association("Tags").Replace(&tags); err != nil {
|
||
return c.Redirect().To("/admin/content/posts?error=Tag+güncelleme+başarısız")
|
||
}
|
||
}
|
||
}
|
||
|
||
// Image processing
|
||
width, _ := strconv.Atoi(c.FormValue("width"))
|
||
height, _ := strconv.Atoi(c.FormValue("height"))
|
||
quality, _ := strconv.Atoi(c.FormValue("quality"))
|
||
format := c.FormValue("format")
|
||
|
||
imagePath, err := services.ProcessAndSaveImage(c, "image", services.ImageOptions{
|
||
Width: width,
|
||
Height: height,
|
||
Quality: quality,
|
||
Format: format,
|
||
Folder: "posts",
|
||
})
|
||
if err == nil && imagePath != "" {
|
||
// Ana görüntü
|
||
post.Images = imagePath
|
||
post.Width = width
|
||
post.Height = height
|
||
post.Quality = quality
|
||
if format != "" {
|
||
post.Format = format
|
||
}
|
||
|
||
// Orta boy (400x300)
|
||
midPath, errMid := services.ProcessAndSaveImage(c, "image", services.ImageOptions{
|
||
Width: 400,
|
||
Height: 300,
|
||
Quality: quality,
|
||
Format: format,
|
||
Folder: "posts",
|
||
})
|
||
if errMid != nil {
|
||
return c.Redirect().To("/admin/content/posts?error=Orta+boy+resim+oluşturulamadı")
|
||
}
|
||
post.ImagesMid = midPath
|
||
|
||
// Küçük boy (48x48)
|
||
minPath, errMin := services.ProcessAndSaveImage(c, "image", services.ImageOptions{
|
||
Width: 48,
|
||
Height: 48,
|
||
Quality: quality,
|
||
Format: format,
|
||
Folder: "posts",
|
||
})
|
||
if errMin != nil {
|
||
return c.Redirect().To("/admin/content/posts?error=Küçük+boy+resim+oluşturulamadı")
|
||
}
|
||
post.ImagesMin = minPath
|
||
} else if err != nil {
|
||
configs.Logger.Error("AdminPostUpdate image upload failed", zap.Error(err))
|
||
return c.Redirect().To("/admin/content/posts?error=Image+API+key+gecersiz+veya+suresi+dolmus")
|
||
} else {
|
||
configs.Logger.Warn("AdminPostUpdate image upload skipped or empty result")
|
||
}
|
||
|
||
if err := dbConfig.DB.Save(&post).Error; err != nil {
|
||
return c.Redirect().To("/admin/content/posts?error=Güncelleme+başarısız")
|
||
}
|
||
return c.Redirect().To("/admin/content/posts?success=Yazı+güncellendi")
|
||
}
|
||
|
||
// AdminPostDelete soft-delete
|
||
func AdminPostDelete(c fiber.Ctx) error {
|
||
id := c.Params("id")
|
||
if err := dbConfig.DB.Delete(&models.Post{}, id).Error; err != nil {
|
||
return c.Redirect().To("/admin/content/posts?error=Silme+başarısız")
|
||
}
|
||
return c.Redirect().To("/admin/content/posts?success=Yazı+silindi")
|
||
}
|
||
|
||
// AdminPostRestore restores soft-deleted post
|
||
func AdminPostRestore(c fiber.Ctx) error {
|
||
id := c.Params("id")
|
||
if err := dbConfig.DB.Unscoped().Model(&models.Post{}).Where("id = ?", id).Update("deleted_at", nil).Error; err != nil {
|
||
return c.Redirect().To("/admin/content/posts?error=Geri+yükleme+başarısız")
|
||
}
|
||
return c.Redirect().To("/admin/content/posts?deleted=true&success=Yazı+geri+yüklendi")
|
||
}
|