Files
ares/controllers/admin_controller.go
Beyhan Oğur 4d92991817 first commit
2026-04-26 21:30:42 +03:00

1642 lines
50 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"
"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 {
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{}
}
// 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
}
// 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
}
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ı")
}
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 (Update if new file provided)
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 && 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,
}
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 {
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
}
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 {
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
}
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")
}