Files
mailgo/internal/web/middleware/auth.go
T
dsh c881146a40 feat(web): 多语言界面(中/英/日)与用户语言偏好字段
- 新增 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 通过
2026-08-20 00:21:10 -04:00

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