Add home_welcome and home_subtitle fields (zh/en) to site_settings so
the home page heading and subtitle are editable from the admin panel,
falling back to the i18n defaults when empty. Wire them through the
config cache, SetUserContext, DefaultData, and home.html.
Fix the /admin/settings/site page rendering blank: Go templates cannot
invoke methods on an interface{}-wrapped struct, so pre-compute
SiteLogoIsURL in the handler instead of calling .Site.LogoIsURL() in
the template.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
337 lines
9.7 KiB
Go
337 lines
9.7 KiB
Go
package handlers
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gin-contrib/sessions"
|
|
"github.com/gin-gonic/gin"
|
|
"gorm.io/gorm"
|
|
|
|
"go_blog/models"
|
|
)
|
|
|
|
// mbToBytes converts a megabyte count (string) to bytes. Returns 0 on parse
|
|
// failure. Values <= 0 are treated as 0 (meaning "use default" for per-type
|
|
// limits).
|
|
func mbToBytes(s string) int64 {
|
|
s = strings.TrimSpace(s)
|
|
if s == "" {
|
|
return 0
|
|
}
|
|
n, err := strconv.ParseFloat(s, 64)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
return int64(n * 1024 * 1024)
|
|
}
|
|
|
|
// bytesToMB renders a byte count as megabytes (one decimal) for form display.
|
|
func bytesToMB(b int64) string {
|
|
return fmt.Sprintf("%.1f", float64(b)/float64(1024*1024))
|
|
}
|
|
|
|
// userIDFromSession extracts the logged-in user's ID, or 0 if absent.
|
|
func userIDFromSession(c *gin.Context) uint {
|
|
session := sessions.Default(c)
|
|
if id, ok := session.Get("user_id").(uint); ok {
|
|
return id
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// ---------------- Site settings ----------------
|
|
|
|
// SiteSettingsPage renders the site display settings form.
|
|
func SiteSettingsPage(db *gorm.DB) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
tr := getTr(c)
|
|
var s models.SiteSetting
|
|
if err := db.First(&s, 1).Error; err != nil {
|
|
s = models.SiteSetting{ID: 1}
|
|
}
|
|
|
|
data := DefaultData(c)
|
|
data["Title"] = tr["settings_site_title"]
|
|
data["Site"] = s
|
|
// Pre-compute derived values so the template never invokes methods on
|
|
// an interface{}-wrapped struct (which Go templates cannot resolve).
|
|
data["SiteLogoIsURL"] = s.LogoIsURL()
|
|
if msg := c.Query("saved"); msg == "1" {
|
|
data["Success"] = tr["settings_saved"]
|
|
}
|
|
c.HTML(http.StatusOK, "settings_site", data)
|
|
}
|
|
}
|
|
|
|
// SiteSettingsSave handles logo upload and text fields for site settings.
|
|
func SiteSettingsSave(db *gorm.DB, storagePath string) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
var s models.SiteSetting
|
|
if err := db.First(&s, 1).Error; err != nil {
|
|
s = models.SiteSetting{ID: 1}
|
|
}
|
|
|
|
s.LogoTextZh = strings.TrimSpace(c.PostForm("logo_text_zh"))
|
|
s.LogoTextEn = strings.TrimSpace(c.PostForm("logo_text_en"))
|
|
s.HeaderTextZh = strings.TrimSpace(c.PostForm("header_text_zh"))
|
|
s.HeaderTextEn = strings.TrimSpace(c.PostForm("header_text_en"))
|
|
s.HomeWelcomeZh = strings.TrimSpace(c.PostForm("home_welcome_zh"))
|
|
s.HomeWelcomeEn = strings.TrimSpace(c.PostForm("home_welcome_en"))
|
|
s.HomeSubtitleZh = strings.TrimSpace(c.PostForm("home_subtitle_zh"))
|
|
s.HomeSubtitleEn = strings.TrimSpace(c.PostForm("home_subtitle_en"))
|
|
s.FooterTextZh = strings.TrimSpace(c.PostForm("footer_text_zh"))
|
|
s.FooterTextEn = strings.TrimSpace(c.PostForm("footer_text_en"))
|
|
s.UpdatedBy = userIDFromSession(c)
|
|
|
|
// Logo upload (optional). A logo_url form field takes precedence over an
|
|
// uploaded file, so admins can set either a local file or an external link.
|
|
if logoURL := strings.TrimSpace(c.PostForm("logo_url")); logoURL != "" {
|
|
s.Logo = logoURL
|
|
} else if file, header, err := c.Request.FormFile("logo"); err == nil {
|
|
defer file.Close()
|
|
check := ValidateUpload(header)
|
|
if !check.OK || check.Type.Category != models.CategoryImage {
|
|
c.Redirect(http.StatusFound, "/admin/settings/site")
|
|
return
|
|
}
|
|
logoDir := filepath.Join(storagePath, "logos")
|
|
os.MkdirAll(logoDir, 0755)
|
|
// Remove the previous local logo (skip external URLs).
|
|
if s.Logo != "" && !s.LogoIsURL() {
|
|
os.Remove(filepath.Join(logoDir, s.Logo))
|
|
}
|
|
ext := strings.ToLower(filepath.Ext(header.Filename))
|
|
savedName := fmt.Sprintf("logo%s", ext)
|
|
dst, err := os.Create(filepath.Join(logoDir, savedName))
|
|
if err != nil {
|
|
c.Redirect(http.StatusFound, "/admin/settings/site")
|
|
return
|
|
}
|
|
defer dst.Close()
|
|
if _, err := io.Copy(dst, file); err != nil {
|
|
c.Redirect(http.StatusFound, "/admin/settings/site")
|
|
return
|
|
}
|
|
s.Logo = savedName
|
|
}
|
|
|
|
// Remove logo entirely if requested.
|
|
if c.PostForm("logo_clear") == "1" {
|
|
if s.Logo != "" && !s.LogoIsURL() {
|
|
os.Remove(filepath.Join(storagePath, "logos", s.Logo))
|
|
}
|
|
s.Logo = ""
|
|
}
|
|
|
|
if err := db.Save(&s).Error; err != nil {
|
|
c.Redirect(http.StatusFound, "/admin/settings/site")
|
|
return
|
|
}
|
|
models.RefreshConfigCache(db)
|
|
c.Redirect(http.StatusFound, "/admin/settings/site?saved=1")
|
|
}
|
|
}
|
|
|
|
// ---------------- Upload settings ----------------
|
|
|
|
// fileTypeView augments an UploadFileType with a pre-rendered max-size MB
|
|
// string for the template (avoids needing a template FuncMap for division).
|
|
type fileTypeView struct {
|
|
models.UploadFileType
|
|
MaxSizeMB string
|
|
}
|
|
|
|
// UploadSettingsPage renders the upload policy + file-type management page.
|
|
func UploadSettingsPage(db *gorm.DB) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
tr := getTr(c)
|
|
var u models.UploadConfig
|
|
if err := db.First(&u, 1).Error; err != nil {
|
|
u = models.UploadConfig{ID: 1, Enabled: true, DefaultMaxSize: models.DefaultUploadMaxSize, StorageDir: "attachments"}
|
|
}
|
|
var types []models.UploadFileType
|
|
db.Order("category asc, sort asc, id asc").Find(&types)
|
|
|
|
views := make([]fileTypeView, 0, len(types))
|
|
for _, t := range types {
|
|
views = append(views, fileTypeView{UploadFileType: t, MaxSizeMB: bytesToMB(t.MaxSize)})
|
|
}
|
|
|
|
data := DefaultData(c)
|
|
data["Title"] = tr["settings_upload_title"]
|
|
data["Upload"] = u
|
|
data["FileTypes"] = views
|
|
data["DefaultMaxSizeMB"] = bytesToMB(u.DefaultMaxSize)
|
|
if msg := c.Query("saved"); msg == "1" {
|
|
data["Success"] = tr["settings_saved"]
|
|
}
|
|
c.HTML(http.StatusOK, "settings_upload", data)
|
|
}
|
|
}
|
|
|
|
// UploadSettingsSave dispatches upload-config and file-type actions.
|
|
func UploadSettingsSave(db *gorm.DB) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
switch c.PostForm("action") {
|
|
case "save_config":
|
|
saveUploadConfig(db, c)
|
|
case "add_type":
|
|
addUploadFileType(db, c)
|
|
case "toggle_type":
|
|
toggleUploadFileType(db, c)
|
|
case "size_type":
|
|
sizeUploadFileType(db, c)
|
|
case "delete_type":
|
|
deleteUploadFileType(db, c)
|
|
}
|
|
models.RefreshConfigCache(db)
|
|
c.Redirect(http.StatusFound, "/admin/settings/upload?saved=1")
|
|
}
|
|
}
|
|
|
|
func saveUploadConfig(db *gorm.DB, c *gin.Context) {
|
|
var u models.UploadConfig
|
|
if err := db.First(&u, 1).Error; err != nil {
|
|
u = models.UploadConfig{ID: 1}
|
|
}
|
|
u.Enabled = c.PostForm("enabled") == "1"
|
|
u.DefaultMaxSize = mbToBytes(c.PostForm("default_max_size"))
|
|
if u.DefaultMaxSize <= 0 {
|
|
u.DefaultMaxSize = models.DefaultUploadMaxSize
|
|
}
|
|
if dir := strings.TrimSpace(c.PostForm("storage_dir")); dir != "" {
|
|
u.StorageDir = dir
|
|
}
|
|
u.UpdatedBy = userIDFromSession(c)
|
|
db.Save(&u)
|
|
}
|
|
|
|
func addUploadFileType(db *gorm.DB, c *gin.Context) {
|
|
ext := strings.ToLower(strings.TrimSpace(c.PostForm("extension")))
|
|
if ext == "" {
|
|
return
|
|
}
|
|
if !strings.HasPrefix(ext, ".") {
|
|
ext = "." + ext
|
|
}
|
|
t := models.UploadFileType{
|
|
Extension: ext,
|
|
MimeType: strings.TrimSpace(c.PostForm("mime_type")),
|
|
Category: strings.TrimSpace(c.PostForm("category")),
|
|
MaxSize: mbToBytes(c.PostForm("max_size")),
|
|
Enabled: c.PostForm("enabled") != "0",
|
|
Sort: 50,
|
|
}
|
|
if t.Category == "" {
|
|
t.Category = models.CategoryOther
|
|
}
|
|
// Ignore duplicate-extension errors silently.
|
|
db.Where("extension = ?", t.Extension).FirstOrCreate(&t)
|
|
}
|
|
|
|
func toggleUploadFileType(db *gorm.DB, c *gin.Context) {
|
|
id, _ := strconv.Atoi(c.PostForm("id"))
|
|
var t models.UploadFileType
|
|
if db.First(&t, id).Error != nil {
|
|
return
|
|
}
|
|
t.Enabled = !t.Enabled
|
|
db.Save(&t)
|
|
}
|
|
|
|
func sizeUploadFileType(db *gorm.DB, c *gin.Context) {
|
|
id, _ := strconv.Atoi(c.PostForm("id"))
|
|
var t models.UploadFileType
|
|
if db.First(&t, id).Error != nil {
|
|
return
|
|
}
|
|
t.MaxSize = mbToBytes(c.PostForm("max_size"))
|
|
db.Save(&t)
|
|
}
|
|
|
|
func deleteUploadFileType(db *gorm.DB, c *gin.Context) {
|
|
id, _ := strconv.Atoi(c.PostForm("id"))
|
|
db.Delete(&models.UploadFileType{}, id)
|
|
}
|
|
|
|
// ---------------- Download settings ----------------
|
|
|
|
// DownloadSettingsPage renders the download base-URL management page.
|
|
func DownloadSettingsPage(db *gorm.DB) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
tr := getTr(c)
|
|
var urls []models.DownloadBaseURL
|
|
db.Order("is_default desc, priority asc, id asc").Find(&urls)
|
|
|
|
data := DefaultData(c)
|
|
data["Title"] = tr["settings_download_title"]
|
|
data["BaseURLs"] = urls
|
|
if msg := c.Query("saved"); msg == "1" {
|
|
data["Success"] = tr["settings_saved"]
|
|
}
|
|
c.HTML(http.StatusOK, "settings_download", data)
|
|
}
|
|
}
|
|
|
|
// DownloadSettingsSave dispatches download base-URL actions.
|
|
func DownloadSettingsSave(db *gorm.DB) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
switch c.PostForm("action") {
|
|
case "add":
|
|
addDownloadBaseURL(db, c)
|
|
case "toggle":
|
|
toggleDownloadBaseURL(db, c)
|
|
case "default":
|
|
defaultDownloadBaseURL(db, c)
|
|
case "delete":
|
|
deleteDownloadBaseURL(db, c)
|
|
}
|
|
models.RefreshConfigCache(db)
|
|
c.Redirect(http.StatusFound, "/admin/settings/download?saved=1")
|
|
}
|
|
}
|
|
|
|
func addDownloadBaseURL(db *gorm.DB, c *gin.Context) {
|
|
name := strings.TrimSpace(c.PostForm("name"))
|
|
base := strings.TrimSpace(c.PostForm("base_url"))
|
|
if base == "" {
|
|
return
|
|
}
|
|
prio, _ := strconv.Atoi(c.PostForm("priority"))
|
|
b := models.DownloadBaseURL{
|
|
Name: name,
|
|
BaseURL: base,
|
|
Priority: prio,
|
|
Enabled: c.PostForm("enabled") != "0",
|
|
}
|
|
db.Create(&b)
|
|
}
|
|
|
|
func toggleDownloadBaseURL(db *gorm.DB, c *gin.Context) {
|
|
id, _ := strconv.Atoi(c.PostForm("id"))
|
|
var b models.DownloadBaseURL
|
|
if db.First(&b, id).Error != nil {
|
|
return
|
|
}
|
|
b.Enabled = !b.Enabled
|
|
db.Save(&b)
|
|
}
|
|
|
|
func defaultDownloadBaseURL(db *gorm.DB, c *gin.Context) {
|
|
id, _ := strconv.Atoi(c.PostForm("id"))
|
|
// Only one default at a time.
|
|
db.Model(&models.DownloadBaseURL{}).Where("1=1").Update("is_default", false)
|
|
db.Model(&models.DownloadBaseURL{}).Where("id = ?", id).Update("is_default", true)
|
|
}
|
|
|
|
func deleteDownloadBaseURL(db *gorm.DB, c *gin.Context) {
|
|
id, _ := strconv.Atoi(c.PostForm("id"))
|
|
db.Delete(&models.DownloadBaseURL{}, id)
|
|
}
|