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("
Turnstile doğrulaması başarısız.
")
}
// 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("Hatalı e-posta veya şifre.
")
}
// Check password
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
return c.Status(fiber.StatusOK).SendString("Hatalı e-posta veya şifre.
")
}
// Check if admin
if user.IsAdmin == nil || !*user.IsAdmin {
return c.Status(fiber.StatusOK).SendString("Bu alana erişim yetkiniz yok.
")
}
// 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("Oturum oluşturulamadı.
")
}
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("Lütfen tüm alanları doldurun.
")
}
// Hash password
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return c.Status(fiber.StatusInternalServerError).SendString("Şifre oluşturulurken hata.
")
}
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("Kullanıcı oluşturulurken hata: " + err.Error() + "
")
}
// 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("Şifre güncellenirken hata.
")
}
user.Password = string(hashedPassword)
}
if err := dbConfig.DB.Save(&user).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).SendString("Güncelleme hatası: " + err.Error() + "
")
}
// 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("Form verileri okunamadı.
")
}
// 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("Ayarlar kaydedilirken hata oluştu.
")
}
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")
}