feat: 品牌CMS配置化 + cookie_secure配置

- config.yaml 新增 server.cookie_secure 配置项
- site_settings 新增 site_name/site_tagline/contact_email 站点信息
- SiteInfo 结构体扩展,支持运行时从 DB 读取站点品牌信息
- cookie.go 使用 cfg.Server.CookieSecure 替代 Mode!=debug 判断
- SetSessionCookie/ClearSessionCookie 支持 siteSettings 运行时覆盖
- 模板替换硬编码 MetaLab → {{.SiteName}}(nav/header/footer/home/login/register/guidelines/admin,12+处)
- 管理后台站点设置页新增站点名称/标语/联系邮箱输入项
- 页脚版权年份改为动态 CurrentYear
- 注册成功消息 CMS 化(后端+前端)
This commit is contained in:
2026-06-21 23:12:18 +08:00
parent 7b5856e0ba
commit 0014bdb9ff
21 changed files with 106 additions and 35 deletions

View File

@ -17,8 +17,8 @@ const (
// SetSessionCookie 写入会话 ID Cookie
// rememberMe=true → Cookie maxAge=30天关浏览器后仍保持登录
// rememberMe=false → Cookie maxAge=空闲超时默认2h与Redis TTL一致关浏览器后自动清除
func SetSessionCookie(c *gin.Context, sid string, rememberMe bool, cfg *config.Config) {
secure := cfg.Server.Mode != "debug"
func SetSessionCookie(c *gin.Context, sid string, rememberMe bool, cfg *config.Config, siteSettings *config.SiteSettings) {
secure := siteSettings.CookieSecure()
c.SetSameSite(http.SameSiteLaxMode)
var maxAge int
@ -33,8 +33,8 @@ func SetSessionCookie(c *gin.Context, sid string, rememberMe bool, cfg *config.C
}
// ClearSessionCookie 清除会话 ID Cookielogout 调用)
func ClearSessionCookie(c *gin.Context, cfg *config.Config) {
secure := cfg.Server.Mode != "debug"
func ClearSessionCookie(c *gin.Context, cfg *config.Config, siteSettings *config.SiteSettings) {
secure := siteSettings.CookieSecure()
c.SetCookie(SessionCookieName, "", -1, "/", "", secure, true)
}

View File

@ -11,6 +11,7 @@ import (
"path/filepath"
"regexp"
"strconv"
"time"
"metazone.cc/mce/internal/model"
@ -159,10 +160,20 @@ func RedirectToLogin(c *gin.Context) {
// injectSiteInfo 从 context 注入站点展示信息
func injectSiteInfo(c *gin.Context, data gin.H) {
if siteName, exists := c.Get("site_name"); exists {
data["SiteName"] = siteName
}
if siteTagline, exists := c.Get("site_tagline"); exists {
data["SiteTagline"] = siteTagline
}
if contactEmail, exists := c.Get("site_contact_email"); exists {
data["ContactEmail"] = contactEmail
}
if framework, exists := c.Get("site_framework"); exists {
data["Framework"] = framework
}
if copyrightStart, exists := c.Get("site_copyright_start"); exists {
data["CopyrightStart"] = copyrightStart
}
data["CurrentYear"] = time.Now().Year()
}

View File

@ -29,8 +29,9 @@ type AuditConfig struct {
}
type ServerConfig struct {
Port string `mapstructure:"port"`
Mode string `mapstructure:"mode"`
Port string `mapstructure:"port"`
Mode string `mapstructure:"mode"`
CookieSecure bool `mapstructure:"cookie_secure"`
}
type DatabaseConfig struct {

View File

@ -112,18 +112,29 @@ func (s *SiteSettings) IsAuditTypeEnabled(auditType string, defaultVal bool) boo
// SiteInfoDefaults 站点展示信息的默认值
type SiteInfoDefaults struct {
Framework string // 附加标识,默认 "Framework: MetaZone"
SiteName string // 站点名称,默认 "MetaLab"
SiteTagline string // 站点标语,默认 "下一代开发者社区"
ContactEmail string // 联系/申诉邮箱,默认 "metazone@foxmail.com"
Framework string // 附加标识,默认 "Framework: MetaGenesis"
CopyrightStart string // 版权起始年份,默认 "2024"
}
// SiteInfo 获取站点展示信息(优先 DB 设置fallback 默认值)
func (s *SiteSettings) SiteInfo() SiteInfoDefaults {
return SiteInfoDefaults{
Framework: s.Get("site.framework", "Framework: MetaZone"),
SiteName: s.Get("site.name", "MetaLab"),
SiteTagline: s.Get("site.tagline", "下一代开发者社区"),
ContactEmail: s.Get("site.contact_email", "metazone@foxmail.com"),
Framework: s.Get("site.framework", "Framework: MetaGenesis"),
CopyrightStart: s.Get("site.copyright_start", "2024"),
}
}
// CookieSecure 返回 cookie_secure 配置值优先站点设置fallback config.yaml
func (s *SiteSettings) CookieSecure() bool {
return s.GetBool("site.cookie_secure", App.Server.CookieSecure)
}
// IsRegistrationEnabled 是否允许新用户注册
func (s *SiteSettings) IsRegistrationEnabled() bool {
return s.GetBool("registration.enabled", true)

View File

@ -78,7 +78,7 @@ func (ac *AuthController) Login(c *gin.Context) {
return
}
common.SetSessionCookie(c, sid, req.RememberMe, ac.cfg)
common.SetSessionCookie(c, sid, req.RememberMe, ac.cfg, ac.siteSettings)
common.OkWithMessage(c, user, "登录成功")
}
@ -108,6 +108,6 @@ func (ac *AuthController) ConfirmRestore(c *gin.Context) {
return
}
common.SetSessionCookie(c, sid, req.RememberMe, ac.cfg)
common.SetSessionCookie(c, sid, req.RememberMe, ac.cfg, ac.siteSettings)
common.OkWithMessage(c, user, "注销已撤销,欢迎回来")
}

View File

@ -69,9 +69,9 @@ func (ac *AuthController) Register(c *gin.Context) {
return
}
common.SetSessionCookie(c, sid, req.RememberMe, ac.cfg)
common.SetSessionCookie(c, sid, req.RememberMe, ac.cfg, ac.siteSettings)
common.OkWithMessage(c, user, "注册成功!欢迎加入 MetaLab")
common.OkWithMessage(c, user, "注册成功!欢迎加入 "+ac.siteSettings.SiteInfo().SiteName)
}
// Logout 退出登录:删除服务端 session + 清除 Cookie
@ -79,7 +79,7 @@ func (ac *AuthController) Logout(c *gin.Context) {
if sid, err := c.Cookie(common.SessionCookieName); err == nil && sid != "" {
_ = ac.sessionManager.Destroy(sid)
}
common.ClearSessionCookie(c, ac.cfg)
common.ClearSessionCookie(c, ac.cfg, ac.siteSettings)
common.OkMessage(c, "已退出登录")
}
@ -93,7 +93,7 @@ func (ac *AuthController) RefreshToken(c *gin.Context) {
s, err := ac.sessionManager.Validate(sid)
if err != nil || s == nil {
common.ClearSessionCookie(c, ac.cfg)
common.ClearSessionCookie(c, ac.cfg, ac.siteSettings)
common.Error(c, http.StatusUnauthorized, "登录凭证已失效,请重新登录")
return
}

View File

@ -3,6 +3,7 @@ package controller
import (
"html/template"
"net/http"
"strings"
"metazone.cc/mce/internal/common"
"metazone.cc/mce/internal/config"
@ -45,6 +46,12 @@ func (ac *AuthController) RegisterPage(c *gin.Context) {
}
guidelines = content
}
// 替换准则中的硬编码站点名和邮箱
siteInfo := ac.siteSettings.SiteInfo()
guidelines = template.HTML(strings.ReplaceAll(
strings.ReplaceAll(string(guidelines), "MetaLab", siteInfo.SiteName),
"metazone@foxmail.com", siteInfo.ContactEmail,
))
c.HTML(http.StatusOK, "auth/register.html", common.BuildPageData(c, gin.H{
"Title": "注册",
"ExtraCSS": "/static/css/auth.css",

View File

@ -22,6 +22,7 @@ type AuthMiddleware struct {
cfg *config.Config
sessionManager *session.Manager
levelSvc autoCheckIner
siteSettings *config.SiteSettings
}
// NewAuthMiddleware 构造函数
@ -29,6 +30,12 @@ func NewAuthMiddleware(cfg *config.Config, sm *session.Manager) *AuthMiddleware
return &AuthMiddleware{cfg: cfg, sessionManager: sm}
}
// WithSiteSettings 链式注入站点设置(用于 cookie_secure 运行时覆盖)
func (am *AuthMiddleware) WithSiteSettings(ss *config.SiteSettings) *AuthMiddleware {
am.siteSettings = ss
return am
}
// WithLevelService 链式注入等级服务(用于自动签到)
func (am *AuthMiddleware) WithLevelService(svc autoCheckIner) *AuthMiddleware {
am.levelSvc = svc
@ -60,7 +67,7 @@ func (am *AuthMiddleware) Required() gin.HandlerFunc {
// abortUnauthorized 统一处理未认证API 返回 JSON页面请求跳转登录
func (am *AuthMiddleware) abortUnauthorized(c *gin.Context) {
common.ClearSessionCookie(c, am.cfg)
common.ClearSessionCookie(c, am.cfg, am.siteSettings)
if strings.HasPrefix(c.Request.URL.Path, "/api/") {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"success": false, "message": "登录已过期,请重新登录",
@ -81,7 +88,7 @@ func (am *AuthMiddleware) Optional() gin.HandlerFunc {
s, err := am.sessionManager.Validate(sid)
if err != nil || s == nil {
common.ClearSessionCookie(c, am.cfg)
common.ClearSessionCookie(c, am.cfg, am.siteSettings)
c.Next()
return
}

View File

@ -14,7 +14,7 @@ func (am *AuthMiddleware) AdminAuth() gin.HandlerFunc {
return func(c *gin.Context) {
sid, err := c.Cookie(common.SessionCookieName)
if err != nil || sid == "" {
common.ClearSessionCookie(c, am.cfg)
common.ClearSessionCookie(c, am.cfg, am.siteSettings)
c.Redirect(http.StatusFound, "/")
c.Abort()
return
@ -23,7 +23,7 @@ func (am *AuthMiddleware) AdminAuth() gin.HandlerFunc {
s, err := am.sessionManager.Validate(sid)
if err != nil || s == nil {
log.Printf("[AdminAuth] session invalid path=%s → 302", c.Request.URL.Path)
common.ClearSessionCookie(c, am.cfg)
common.ClearSessionCookie(c, am.cfg, am.siteSettings)
c.Redirect(http.StatusFound, "/")
c.Abort()
return

View File

@ -11,6 +11,9 @@ import (
func InjectSiteInfo(siteSettings *config.SiteSettings) gin.HandlerFunc {
return func(c *gin.Context) {
info := siteSettings.SiteInfo()
c.Set("site_name", info.SiteName)
c.Set("site_tagline", info.SiteTagline)
c.Set("site_contact_email", info.ContactEmail)
c.Set("site_framework", info.Framework)
c.Set("site_copyright_start", info.CopyrightStart)
c.Set("is_maintenance", siteSettings.IsMaintenanceEnabled())

View File

@ -80,7 +80,7 @@ func buildDepsCore(db *gorm.DB, cfg *config.Config, siteSettings *config.SiteSet
rateLimiter := middleware.NewRateLimiter()
authCtrl := controller.NewAuthController(authService, sessionMgr, rateLimiter, cfg, siteSettings)
avatarSvc := service.NewAvatarService(userRepo)
authMdw := middleware.NewAuthMiddleware(cfg, sessionMgr)
authMdw := middleware.NewAuthMiddleware(cfg, sessionMgr).WithSiteSettings(siteSettings)
return userRepo, sessionMgr, authService, rateLimiter, authCtrl, avatarSvc, authMdw
}