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") }