- 新增 internal/i18n 包:以原始中文字符串为 key 的翻译目录(zh 恒等, en 兜底,ja 缺译回退 en),t/tf 模板函数与 Accept-Language 解析 (按 q 值排序,兜底英语) - User 模型新增 language 字段(auto|en|zh|ja,默认 auto), AutoMigrate 自动加列;UserStore 新增 UpdateLanguage - 语言解析:登录用户按偏好(auto 时取浏览器 Accept-Language), 登录/封禁页按浏览器语言;所有 c.HTML 渲染注入 Lang 模板数据 - 全部页面模板(用户页 + 管理后台 12 页)接入翻译:导航、文件夹、 邮件列表/阅读/写信、设置、登录、封禁页、表单、表格、JS 确认框等; html lang 属性随语言切换;日期函数(shortDate/time12/time12m) 与 folderLabel 改为语言感知(上午/下午 vs AM/PM vs 午前/午後) - 设置页新增「界面语言」选择(自动/English/中文/日本語), /settings 表单支持单独提交语言偏好;管理后台用户表单可预设语言 - 封禁原因等入库中文文案在展示时经 t 翻译(未知 key 原样回退) - 测试:i18n 单测(解析/回退/译文)、render 测试补 Lang 数据、 时间格式化测试适配新签名;全量 go test 通过
113 lines
2.8 KiB
Go
113 lines
2.8 KiB
Go
package middleware
|
|
|
|
import (
|
|
"time"
|
|
|
|
"mail_go/internal/i18n"
|
|
"mail_go/internal/store"
|
|
|
|
"github.com/gin-contrib/sessions"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
const (
|
|
// sessionAbsoluteMaxAge 会话绝对过期时间:超过后强制重新登录。
|
|
sessionAbsoluteMaxAge = 7 * 24 * time.Hour
|
|
// sessionSlidingRefresh 滑动续期阈值:距上次刷新超过该时长则更新
|
|
// loginAt 并写回 cookie,保持活跃用户不中断(约 12 小时写回一次)。
|
|
sessionSlidingRefresh = 12 * time.Hour
|
|
)
|
|
|
|
// sessionInt64 兼容不同底层 session store 解码出的整数类型。
|
|
func sessionInt64(v interface{}) (int64, bool) {
|
|
switch n := v.(type) {
|
|
case int64:
|
|
return n, true
|
|
case int:
|
|
return int64(n), true
|
|
case uint:
|
|
return int64(n), true
|
|
case uint64:
|
|
return int64(n), true
|
|
case float64:
|
|
return int64(n), true
|
|
default:
|
|
return 0, false
|
|
}
|
|
}
|
|
|
|
// AuthMiddleware checks for a valid session and loads the current user
|
|
// into the Gin context. If no valid session exists, it redirects to /login.
|
|
func AuthMiddleware(stores *store.Stores) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
session := sessions.Default(c)
|
|
userID := session.Get("userID")
|
|
if userID == nil {
|
|
c.Redirect(302, "/login")
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
// 会话绝对过期:登录超过 7 天强制重新登录;
|
|
// 滑动续期:活跃会话每 12 小时刷新一次 loginAt。
|
|
if loginAt, ok := sessionInt64(session.Get("loginAt")); ok {
|
|
elapsed := time.Since(time.Unix(loginAt, 0))
|
|
if elapsed > sessionAbsoluteMaxAge {
|
|
session.Clear()
|
|
session.Save()
|
|
c.Redirect(302, "/login")
|
|
c.Abort()
|
|
return
|
|
}
|
|
if elapsed > sessionSlidingRefresh {
|
|
session.Set("loginAt", time.Now().Unix())
|
|
session.Save()
|
|
}
|
|
}
|
|
|
|
// userID is stored as uint in session, but sessions.Get returns interface{}
|
|
// which may be stored as int or uint depending on the underlying store.
|
|
var id uint
|
|
switch v := userID.(type) {
|
|
case uint:
|
|
id = v
|
|
case int:
|
|
id = uint(v)
|
|
case int64:
|
|
id = uint(v)
|
|
case float64:
|
|
id = uint(v)
|
|
default:
|
|
session.Clear()
|
|
session.Save()
|
|
c.Redirect(302, "/login")
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
user, err := stores.Users.GetByID(id)
|
|
if err != nil {
|
|
session.Clear()
|
|
session.Save()
|
|
c.Redirect(302, "/login")
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
// 首次登录/密码被重置的用户必须先修改密码才能使用其他功能
|
|
if user.MustChangePassword && c.Request.URL.Path != "/settings" && c.Request.URL.Path != "/logout" {
|
|
c.Redirect(302, "/settings?force=1")
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
// 解析界面语言:用户偏好 auto 时按浏览器 Accept-Language 选择,
|
|
// 最终兜底英语。模板数据与错误提示统一使用该值。
|
|
c.Set("lang", i18n.Resolve(user.Language, c.GetHeader("Accept-Language")))
|
|
|
|
c.Set("currentUser", user)
|
|
c.Set("userID", id)
|
|
c.Next()
|
|
}
|
|
}
|