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:
@ -4,6 +4,7 @@
|
|||||||
server:
|
server:
|
||||||
port: 8080
|
port: 8080
|
||||||
mode: debug # debug | release | test
|
mode: debug # debug | release | test
|
||||||
|
cookie_secure: false # Cookie Secure 标志(生产环境应设为 true,部分反向代理环境下可能需要灵活控制)
|
||||||
|
|
||||||
database:
|
database:
|
||||||
host: 127.0.0.1
|
host: 127.0.0.1
|
||||||
|
|||||||
@ -17,8 +17,8 @@ const (
|
|||||||
// SetSessionCookie 写入会话 ID Cookie
|
// SetSessionCookie 写入会话 ID Cookie
|
||||||
// rememberMe=true → Cookie maxAge=30天,关浏览器后仍保持登录
|
// rememberMe=true → Cookie maxAge=30天,关浏览器后仍保持登录
|
||||||
// rememberMe=false → Cookie maxAge=空闲超时(默认2h),与Redis TTL一致,关浏览器后自动清除
|
// rememberMe=false → Cookie maxAge=空闲超时(默认2h),与Redis TTL一致,关浏览器后自动清除
|
||||||
func SetSessionCookie(c *gin.Context, sid string, rememberMe bool, cfg *config.Config) {
|
func SetSessionCookie(c *gin.Context, sid string, rememberMe bool, cfg *config.Config, siteSettings *config.SiteSettings) {
|
||||||
secure := cfg.Server.Mode != "debug"
|
secure := siteSettings.CookieSecure()
|
||||||
c.SetSameSite(http.SameSiteLaxMode)
|
c.SetSameSite(http.SameSiteLaxMode)
|
||||||
|
|
||||||
var maxAge int
|
var maxAge int
|
||||||
@ -33,8 +33,8 @@ func SetSessionCookie(c *gin.Context, sid string, rememberMe bool, cfg *config.C
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ClearSessionCookie 清除会话 ID Cookie(logout 调用)
|
// ClearSessionCookie 清除会话 ID Cookie(logout 调用)
|
||||||
func ClearSessionCookie(c *gin.Context, cfg *config.Config) {
|
func ClearSessionCookie(c *gin.Context, cfg *config.Config, siteSettings *config.SiteSettings) {
|
||||||
secure := cfg.Server.Mode != "debug"
|
secure := siteSettings.CookieSecure()
|
||||||
c.SetCookie(SessionCookieName, "", -1, "/", "", secure, true)
|
c.SetCookie(SessionCookieName, "", -1, "/", "", secure, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"metazone.cc/mce/internal/model"
|
"metazone.cc/mce/internal/model"
|
||||||
|
|
||||||
@ -159,10 +160,20 @@ func RedirectToLogin(c *gin.Context) {
|
|||||||
|
|
||||||
// injectSiteInfo 从 context 注入站点展示信息
|
// injectSiteInfo 从 context 注入站点展示信息
|
||||||
func injectSiteInfo(c *gin.Context, data gin.H) {
|
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 {
|
if framework, exists := c.Get("site_framework"); exists {
|
||||||
data["Framework"] = framework
|
data["Framework"] = framework
|
||||||
}
|
}
|
||||||
if copyrightStart, exists := c.Get("site_copyright_start"); exists {
|
if copyrightStart, exists := c.Get("site_copyright_start"); exists {
|
||||||
data["CopyrightStart"] = copyrightStart
|
data["CopyrightStart"] = copyrightStart
|
||||||
}
|
}
|
||||||
|
data["CurrentYear"] = time.Now().Year()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,8 +29,9 @@ type AuditConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ServerConfig struct {
|
type ServerConfig struct {
|
||||||
Port string `mapstructure:"port"`
|
Port string `mapstructure:"port"`
|
||||||
Mode string `mapstructure:"mode"`
|
Mode string `mapstructure:"mode"`
|
||||||
|
CookieSecure bool `mapstructure:"cookie_secure"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DatabaseConfig struct {
|
type DatabaseConfig struct {
|
||||||
|
|||||||
@ -112,18 +112,29 @@ func (s *SiteSettings) IsAuditTypeEnabled(auditType string, defaultVal bool) boo
|
|||||||
|
|
||||||
// SiteInfoDefaults 站点展示信息的默认值
|
// SiteInfoDefaults 站点展示信息的默认值
|
||||||
type SiteInfoDefaults struct {
|
type SiteInfoDefaults struct {
|
||||||
Framework string // 附加标识,默认 "Framework: MetaZone"
|
SiteName string // 站点名称,默认 "MetaLab"
|
||||||
|
SiteTagline string // 站点标语,默认 "下一代开发者社区"
|
||||||
|
ContactEmail string // 联系/申诉邮箱,默认 "metazone@foxmail.com"
|
||||||
|
Framework string // 附加标识,默认 "Framework: MetaGenesis"
|
||||||
CopyrightStart string // 版权起始年份,默认 "2024"
|
CopyrightStart string // 版权起始年份,默认 "2024"
|
||||||
}
|
}
|
||||||
|
|
||||||
// SiteInfo 获取站点展示信息(优先 DB 设置,fallback 默认值)
|
// SiteInfo 获取站点展示信息(优先 DB 设置,fallback 默认值)
|
||||||
func (s *SiteSettings) SiteInfo() SiteInfoDefaults {
|
func (s *SiteSettings) SiteInfo() SiteInfoDefaults {
|
||||||
return 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"),
|
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 是否允许新用户注册
|
// IsRegistrationEnabled 是否允许新用户注册
|
||||||
func (s *SiteSettings) IsRegistrationEnabled() bool {
|
func (s *SiteSettings) IsRegistrationEnabled() bool {
|
||||||
return s.GetBool("registration.enabled", true)
|
return s.GetBool("registration.enabled", true)
|
||||||
|
|||||||
@ -78,7 +78,7 @@ func (ac *AuthController) Login(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
common.SetSessionCookie(c, sid, req.RememberMe, ac.cfg)
|
common.SetSessionCookie(c, sid, req.RememberMe, ac.cfg, ac.siteSettings)
|
||||||
|
|
||||||
common.OkWithMessage(c, user, "登录成功")
|
common.OkWithMessage(c, user, "登录成功")
|
||||||
}
|
}
|
||||||
@ -108,6 +108,6 @@ func (ac *AuthController) ConfirmRestore(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
common.SetSessionCookie(c, sid, req.RememberMe, ac.cfg)
|
common.SetSessionCookie(c, sid, req.RememberMe, ac.cfg, ac.siteSettings)
|
||||||
common.OkWithMessage(c, user, "注销已撤销,欢迎回来")
|
common.OkWithMessage(c, user, "注销已撤销,欢迎回来")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -69,9 +69,9 @@ func (ac *AuthController) Register(c *gin.Context) {
|
|||||||
return
|
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
|
// Logout 退出登录:删除服务端 session + 清除 Cookie
|
||||||
@ -79,7 +79,7 @@ func (ac *AuthController) Logout(c *gin.Context) {
|
|||||||
if sid, err := c.Cookie(common.SessionCookieName); err == nil && sid != "" {
|
if sid, err := c.Cookie(common.SessionCookieName); err == nil && sid != "" {
|
||||||
_ = ac.sessionManager.Destroy(sid)
|
_ = ac.sessionManager.Destroy(sid)
|
||||||
}
|
}
|
||||||
common.ClearSessionCookie(c, ac.cfg)
|
common.ClearSessionCookie(c, ac.cfg, ac.siteSettings)
|
||||||
common.OkMessage(c, "已退出登录")
|
common.OkMessage(c, "已退出登录")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,7 +93,7 @@ func (ac *AuthController) RefreshToken(c *gin.Context) {
|
|||||||
|
|
||||||
s, err := ac.sessionManager.Validate(sid)
|
s, err := ac.sessionManager.Validate(sid)
|
||||||
if err != nil || s == nil {
|
if err != nil || s == nil {
|
||||||
common.ClearSessionCookie(c, ac.cfg)
|
common.ClearSessionCookie(c, ac.cfg, ac.siteSettings)
|
||||||
common.Error(c, http.StatusUnauthorized, "登录凭证已失效,请重新登录")
|
common.Error(c, http.StatusUnauthorized, "登录凭证已失效,请重新登录")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package controller
|
|||||||
import (
|
import (
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"metazone.cc/mce/internal/common"
|
"metazone.cc/mce/internal/common"
|
||||||
"metazone.cc/mce/internal/config"
|
"metazone.cc/mce/internal/config"
|
||||||
@ -45,6 +46,12 @@ func (ac *AuthController) RegisterPage(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
guidelines = content
|
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{
|
c.HTML(http.StatusOK, "auth/register.html", common.BuildPageData(c, gin.H{
|
||||||
"Title": "注册",
|
"Title": "注册",
|
||||||
"ExtraCSS": "/static/css/auth.css",
|
"ExtraCSS": "/static/css/auth.css",
|
||||||
|
|||||||
@ -22,6 +22,7 @@ type AuthMiddleware struct {
|
|||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
sessionManager *session.Manager
|
sessionManager *session.Manager
|
||||||
levelSvc autoCheckIner
|
levelSvc autoCheckIner
|
||||||
|
siteSettings *config.SiteSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAuthMiddleware 构造函数
|
// NewAuthMiddleware 构造函数
|
||||||
@ -29,6 +30,12 @@ func NewAuthMiddleware(cfg *config.Config, sm *session.Manager) *AuthMiddleware
|
|||||||
return &AuthMiddleware{cfg: cfg, sessionManager: sm}
|
return &AuthMiddleware{cfg: cfg, sessionManager: sm}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithSiteSettings 链式注入站点设置(用于 cookie_secure 运行时覆盖)
|
||||||
|
func (am *AuthMiddleware) WithSiteSettings(ss *config.SiteSettings) *AuthMiddleware {
|
||||||
|
am.siteSettings = ss
|
||||||
|
return am
|
||||||
|
}
|
||||||
|
|
||||||
// WithLevelService 链式注入等级服务(用于自动签到)
|
// WithLevelService 链式注入等级服务(用于自动签到)
|
||||||
func (am *AuthMiddleware) WithLevelService(svc autoCheckIner) *AuthMiddleware {
|
func (am *AuthMiddleware) WithLevelService(svc autoCheckIner) *AuthMiddleware {
|
||||||
am.levelSvc = svc
|
am.levelSvc = svc
|
||||||
@ -60,7 +67,7 @@ func (am *AuthMiddleware) Required() gin.HandlerFunc {
|
|||||||
|
|
||||||
// abortUnauthorized 统一处理未认证:API 返回 JSON,页面请求跳转登录
|
// abortUnauthorized 统一处理未认证:API 返回 JSON,页面请求跳转登录
|
||||||
func (am *AuthMiddleware) abortUnauthorized(c *gin.Context) {
|
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/") {
|
if strings.HasPrefix(c.Request.URL.Path, "/api/") {
|
||||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||||
"success": false, "message": "登录已过期,请重新登录",
|
"success": false, "message": "登录已过期,请重新登录",
|
||||||
@ -81,7 +88,7 @@ func (am *AuthMiddleware) Optional() gin.HandlerFunc {
|
|||||||
|
|
||||||
s, err := am.sessionManager.Validate(sid)
|
s, err := am.sessionManager.Validate(sid)
|
||||||
if err != nil || s == nil {
|
if err != nil || s == nil {
|
||||||
common.ClearSessionCookie(c, am.cfg)
|
common.ClearSessionCookie(c, am.cfg, am.siteSettings)
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,7 +14,7 @@ func (am *AuthMiddleware) AdminAuth() gin.HandlerFunc {
|
|||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
sid, err := c.Cookie(common.SessionCookieName)
|
sid, err := c.Cookie(common.SessionCookieName)
|
||||||
if err != nil || sid == "" {
|
if err != nil || sid == "" {
|
||||||
common.ClearSessionCookie(c, am.cfg)
|
common.ClearSessionCookie(c, am.cfg, am.siteSettings)
|
||||||
c.Redirect(http.StatusFound, "/")
|
c.Redirect(http.StatusFound, "/")
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
@ -23,7 +23,7 @@ func (am *AuthMiddleware) AdminAuth() gin.HandlerFunc {
|
|||||||
s, err := am.sessionManager.Validate(sid)
|
s, err := am.sessionManager.Validate(sid)
|
||||||
if err != nil || s == nil {
|
if err != nil || s == nil {
|
||||||
log.Printf("[AdminAuth] session invalid path=%s → 302", c.Request.URL.Path)
|
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.Redirect(http.StatusFound, "/")
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
|
|||||||
@ -11,6 +11,9 @@ import (
|
|||||||
func InjectSiteInfo(siteSettings *config.SiteSettings) gin.HandlerFunc {
|
func InjectSiteInfo(siteSettings *config.SiteSettings) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
info := siteSettings.SiteInfo()
|
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_framework", info.Framework)
|
||||||
c.Set("site_copyright_start", info.CopyrightStart)
|
c.Set("site_copyright_start", info.CopyrightStart)
|
||||||
c.Set("is_maintenance", siteSettings.IsMaintenanceEnabled())
|
c.Set("is_maintenance", siteSettings.IsMaintenanceEnabled())
|
||||||
|
|||||||
@ -80,7 +80,7 @@ func buildDepsCore(db *gorm.DB, cfg *config.Config, siteSettings *config.SiteSet
|
|||||||
rateLimiter := middleware.NewRateLimiter()
|
rateLimiter := middleware.NewRateLimiter()
|
||||||
authCtrl := controller.NewAuthController(authService, sessionMgr, rateLimiter, cfg, siteSettings)
|
authCtrl := controller.NewAuthController(authService, sessionMgr, rateLimiter, cfg, siteSettings)
|
||||||
avatarSvc := service.NewAvatarService(userRepo)
|
avatarSvc := service.NewAvatarService(userRepo)
|
||||||
authMdw := middleware.NewAuthMiddleware(cfg, sessionMgr)
|
authMdw := middleware.NewAuthMiddleware(cfg, sessionMgr).WithSiteSettings(siteSettings)
|
||||||
return userRepo, sessionMgr, authService, rateLimiter, authCtrl, avatarSvc, authMdw
|
return userRepo, sessionMgr, authService, rateLimiter, authCtrl, avatarSvc, authMdw
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
<main class="auth-page">
|
<main class="auth-page">
|
||||||
<div class="auth-card">
|
<div class="auth-card">
|
||||||
<h2>登录 MetaLab</h2>
|
<h2>登录 {{.SiteName}}</h2>
|
||||||
<p class="auth-subtitle">回到开发者社区</p>
|
<p class="auth-subtitle">回到开发者社区</p>
|
||||||
<form id="loginForm" novalidate>
|
<form id="loginForm" novalidate>
|
||||||
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{{else if .RegistrationEnabled}}
|
{{else if .RegistrationEnabled}}
|
||||||
<h2>创建账号</h2>
|
<h2>创建账号</h2>
|
||||||
<p class="auth-subtitle">加入 MetaLab 开发者社区</p>
|
<p class="auth-subtitle">加入 {{.SiteName}} 开发者社区</p>
|
||||||
<form id="registerForm" novalidate>
|
<form id="registerForm" novalidate>
|
||||||
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@ -38,7 +38,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<span class="remember-hint">非共享设备推荐</span>
|
<span class="remember-hint">非共享设备推荐</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="guidelines-link">点击注册即表示同意 <a href="#" id="openGuidelines">《MetaLab 社区用户准则》</a></p>
|
<p class="guidelines-link">点击注册即表示同意 <a href="#" id="openGuidelines">《{{.SiteName}} 社区用户准则》</a></p>
|
||||||
<button type="submit" class="submit-btn" id="submitBtn">注 册</button>
|
<button type="submit" class="submit-btn" id="submitBtn">注 册</button>
|
||||||
</form>
|
</form>
|
||||||
{{else}}
|
{{else}}
|
||||||
@ -59,7 +59,7 @@
|
|||||||
<div class="modal-overlay" id="modalOverlay">
|
<div class="modal-overlay" id="modalOverlay">
|
||||||
<div class="modal">
|
<div class="modal">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h3>MetaLab 社区用户准则</h3>
|
<h3>{{.SiteName}} 社区用户准则</h3>
|
||||||
<button class="modal-close" id="modalClose">×</button>
|
<button class="modal-close" id="modalClose">×</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body" id="modalBody">
|
<div class="modal-body" id="modalBody">
|
||||||
|
|||||||
@ -7,8 +7,8 @@
|
|||||||
<section class="hero-banner">
|
<section class="hero-banner">
|
||||||
<div class="hero-bg"></div>
|
<div class="hero-bg"></div>
|
||||||
<div class="hero-content">
|
<div class="hero-content">
|
||||||
<h1 class="hero-title">Meta<span class="hero-accent">Lab</span></h1>
|
<h1 class="hero-title">{{.SiteName}}</h1>
|
||||||
<p class="hero-tagline">下一代开发者社区</p>
|
<p class="hero-tagline">{{.SiteTagline}}</p>
|
||||||
{{if .IsLoggedIn}}
|
{{if .IsLoggedIn}}
|
||||||
<p class="hero-welcome">欢迎回来,{{.Username}} 👋</p>
|
<p class="hero-welcome">欢迎回来,{{.Username}} 👋</p>
|
||||||
{{else}}
|
{{else}}
|
||||||
@ -69,7 +69,7 @@
|
|||||||
<div class="user-avatar-fallback"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg></div>
|
<div class="user-avatar-fallback"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg></div>
|
||||||
{{end}}
|
{{end}}
|
||||||
<h3 class="user-card-name">{{.Username}}</h3>
|
<h3 class="user-card-name">{{.Username}}</h3>
|
||||||
<p class="user-card-desc">欢迎加入 MetaLab</p>
|
<p class="user-card-desc">欢迎加入 {{.SiteName}}</p>
|
||||||
<a href="/space" class="user-card-link">我的空间</a>
|
<a href="/space" class="user-card-link">我的空间</a>
|
||||||
{{else}}
|
{{else}}
|
||||||
<div class="user-avatar-fallback guest-avatar">
|
<div class="user-avatar-fallback guest-avatar">
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<footer>
|
<footer>
|
||||||
<div class="container footer-content">
|
<div class="container footer-content">
|
||||||
<p class="copyright">© {{.CopyrightStart}}-2026 MetaLab{{if .Framework}} | {{.Framework}}{{end}}{{if .CanAccessAdmin}} | <a href="/admin">管理面板</a>{{end}}</p>
|
<p class="copyright">© {{.CopyrightStart}}-{{.CurrentYear}} {{.SiteName}}{{if .Framework}} | {{.Framework}}{{end}}{{if .CanAccessAdmin}} | <a href="/admin">管理面板</a>{{end}}</p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
<script src="/static/js/common.js?v={{assetV "/static/js/common.js"}}" nonce="{{.CSPNonce}}"></script>
|
<script src="/static/js/common.js?v={{assetV "/static/js/common.js"}}" nonce="{{.CSPNonce}}"></script>
|
||||||
|
|||||||
@ -7,12 +7,12 @@
|
|||||||
{{if .OgTitle}}
|
{{if .OgTitle}}
|
||||||
<meta property="og:title" content="{{.OgTitle}}">
|
<meta property="og:title" content="{{.OgTitle}}">
|
||||||
<meta property="og:type" content="article">
|
<meta property="og:type" content="article">
|
||||||
<meta property="og:site_name" content="MetaLab">
|
<meta property="og:site_name" content="{{.SiteName}}">
|
||||||
{{if .OgDescription}}<meta property="og:description" content="{{.OgDescription}}">{{end}}
|
{{if .OgDescription}}<meta property="og:description" content="{{.OgDescription}}">{{end}}
|
||||||
{{if .OgImage}}<meta property="og:image" content="{{.OgImage}}">{{end}}
|
{{if .OgImage}}<meta property="og:image" content="{{.OgImage}}">{{end}}
|
||||||
{{if .OgURL}}<meta property="og:url" content="{{.OgURL}}">{{end}}
|
{{if .OgURL}}<meta property="og:url" content="{{.OgURL}}">{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
<title>{{.Title}} - MetaLab</title>
|
<title>{{.Title}} - {{.SiteName}}</title>
|
||||||
<link rel="stylesheet" href="/static/css/common.css?v={{assetV "/static/css/common.css"}}">
|
<link rel="stylesheet" href="/static/css/common.css?v={{assetV "/static/css/common.css"}}">
|
||||||
{{if .ExtraCSS}}
|
{{if .ExtraCSS}}
|
||||||
<link rel="stylesheet" href="{{.ExtraCSS}}?v={{assetV .ExtraCSS}}">
|
<link rel="stylesheet" href="{{.ExtraCSS}}?v={{assetV .ExtraCSS}}">
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<header>
|
<header>
|
||||||
<div class="container nav-container">
|
<div class="container nav-container">
|
||||||
<a href="/" class="logo">Meta<span>Lab</span></a>
|
<a href="/" class="logo">{{.SiteName}}</a>
|
||||||
<button class="mobile-menu-btn" id="mobileMenuBtn">
|
<button class="mobile-menu-btn" id="mobileMenuBtn">
|
||||||
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
|
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -290,7 +290,7 @@
|
|||||||
try { resp = JSON.parse(xhr.responseText); } catch (err) { resp = null; }
|
try { resp = JSON.parse(xhr.responseText); } catch (err) { resp = null; }
|
||||||
|
|
||||||
if (xhr.status === 200 && resp && resp.success) {
|
if (xhr.status === 200 && resp && resp.success) {
|
||||||
window.MetaLab.toast(toast, '🎉 注册成功!欢迎加入 MetaLab', false);
|
window.MetaLab.toast(toast, resp.message || '🎉 注册成功!', false);
|
||||||
// Token 已通过 HttpOnly Cookie 自动设置,JS 无需处理
|
// Token 已通过 HttpOnly Cookie 自动设置,JS 无需处理
|
||||||
// 注册即登录,跳转首页
|
// 注册即登录,跳转首页
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
{{if .CSRFToken}}<meta name="csrf-token" content="{{.CSRFToken}}">{{end}}
|
{{if .CSRFToken}}<meta name="csrf-token" content="{{.CSRFToken}}">{{end}}
|
||||||
<title>{{.Title}} - MetaLab 管理面板</title>
|
<title>{{.Title}} - {{.SiteName}} 管理面板</title>
|
||||||
<link rel="stylesheet" href="/admin/static/css/common.css?v={{assetV "/admin/static/css/common.css"}}">
|
<link rel="stylesheet" href="/admin/static/css/common.css?v={{assetV "/admin/static/css/common.css"}}">
|
||||||
{{if .ExtraCSS}}
|
{{if .ExtraCSS}}
|
||||||
<link rel="stylesheet" href="{{.ExtraCSS}}">
|
<link rel="stylesheet" href="{{.ExtraCSS}}">
|
||||||
@ -13,7 +13,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<header class="admin-header">
|
<header class="admin-header">
|
||||||
<div class="admin-header-inner">
|
<div class="admin-header-inner">
|
||||||
<a href="/admin" class="admin-logo">MetaLab <span>管理面板</span></a>
|
<a href="/admin" class="admin-logo">{{.SiteName}} <span>管理面板</span></a>
|
||||||
<div class="admin-header-right">
|
<div class="admin-header-right">
|
||||||
<span class="admin-user">{{.Username}}</span>
|
<span class="admin-user">{{.Username}}</span>
|
||||||
<a href="/" class="admin-back-btn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>返回前台</a>
|
<a href="/" class="admin-back-btn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>返回前台</a>
|
||||||
|
|||||||
@ -10,8 +10,38 @@
|
|||||||
<div class="section-body">
|
<div class="section-body">
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<div class="setting-info">
|
<div class="setting-info">
|
||||||
<span class="setting-label">版权信息</span>
|
<span class="setting-label">站点名称</span>
|
||||||
<span class="setting-desc">页脚附加信息,留空则隐藏(如 "Framework: MetaZone")</span>
|
<span class="setting-desc">全站品牌名(导航栏、标题、页脚等位置)</span>
|
||||||
|
</div>
|
||||||
|
<div class="setting-input-wrap">
|
||||||
|
<input type="text" id="setting-site-name" class="setting-input" placeholder="MetaLab" value="{{.SiteName}}">
|
||||||
|
<button class="btn btn-sm btn-primary btn-save" data-key="site.name" data-input="setting-site-name">保存</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="setting-info">
|
||||||
|
<span class="setting-label">站点标语</span>
|
||||||
|
<span class="setting-desc">首页 Hero 区域的副标题</span>
|
||||||
|
</div>
|
||||||
|
<div class="setting-input-wrap">
|
||||||
|
<input type="text" id="setting-site-tagline" class="setting-input" placeholder="下一代开发者社区" value="{{.SiteTagline}}">
|
||||||
|
<button class="btn btn-sm btn-primary btn-save" data-key="site.tagline" data-input="setting-site-tagline">保存</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="setting-info">
|
||||||
|
<span class="setting-label">联系邮箱</span>
|
||||||
|
<span class="setting-desc">申诉/联系邮箱,显示在社区准则等页面</span>
|
||||||
|
</div>
|
||||||
|
<div class="setting-input-wrap">
|
||||||
|
<input type="text" id="setting-site-email" class="setting-input" placeholder="metazone@foxmail.com" value="{{.ContactEmail}}">
|
||||||
|
<button class="btn btn-sm btn-primary btn-save" data-key="site.contact_email" data-input="setting-site-email">保存</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="setting-info">
|
||||||
|
<span class="setting-label">附加信息</span>
|
||||||
|
<span class="setting-desc">页脚附加信息,留空则隐藏(如 "Framework: MetaGenesis")</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-input-wrap">
|
<div class="setting-input-wrap">
|
||||||
<input type="text" id="setting-site-framework" class="setting-input" placeholder="留空则不显示" value="{{.Framework}}">
|
<input type="text" id="setting-site-framework" class="setting-input" placeholder="留空则不显示" value="{{.Framework}}">
|
||||||
|
|||||||
Reference in New Issue
Block a user