feat: 设置页新增登录管理 TAB

- Session 结构体新增 IP、UserAgent、Remark 字段,登录/注册时记录设备信息
- Store 接口新增 ListByUID、DeleteByUIDExclude 方法,MemoryStore 完整实现
- SessionManager 新增 ListByUID、DestroyOtherByUID、UpdateRemark 方法
- 新增 UA 轻量解析器(session/ua.go),提取浏览器/OS/设备类型
- 新增 settings_api_sessions.go,提供列表/踢出/一键踢出/备注 4 个 API
- 控制器层声明 sessionManager 最小接口(ISP),SettingsController 注入依赖
- Auth 中间件注入 sid 到 gin context,支持识别当前会话
- 设置页左侧导航增加登录管理入口,tab 白名单新增 sessions
- 前端实现设备列表展示(浏览器/OS/设备图标/IP/时间)、当前设备标识、备注输入、踢出按钮、一键踢出
- settings.css 新增会话管理全套样式及响应式适配
This commit is contained in:
2026-05-30 23:58:37 +08:00
parent e3853071d6
commit d203447eb3
15 changed files with 884 additions and 13 deletions

View File

@ -6,6 +6,7 @@ import (
"metazone.cc/metalab/internal/common"
"metazone.cc/metalab/internal/model"
"metazone.cc/metalab/internal/session"
"github.com/gin-gonic/gin"
)
@ -37,11 +38,12 @@ type SettingsController struct {
authService profileProvider
avatarService avatarProvider
auditService auditSubmittable
sessionMgr sessionManager
}
// NewSettingsController 构造函数
func NewSettingsController(authService profileProvider, avatarService avatarProvider, auditService auditSubmittable) *SettingsController {
return &SettingsController{authService: authService, avatarService: avatarService, auditService: auditService}
func NewSettingsController(authService profileProvider, avatarService avatarProvider, auditService auditSubmittable, sessionMgr sessionManager) *SettingsController {
return &SettingsController{authService: authService, avatarService: avatarService, auditService: auditService, sessionMgr: sessionMgr}
}
// SettingsPage 个人设置页面(需登录)
@ -55,7 +57,7 @@ func (sc *SettingsController) SettingsPage(c *gin.Context) {
uid := uidVal.(uint)
tab := c.Param("tab")
if tab != "profile" && tab != "account" {
if tab != "profile" && tab != "account" && tab != "sessions" {
c.String(http.StatusNotFound, "页面不存在")
return
}
@ -69,7 +71,7 @@ func (sc *SettingsController) SettingsPage(c *gin.Context) {
// 查询待审核类型(用于前端显示审核提示)
pendingTypes, _ := sc.auditService.GetPendingTypes(uid)
c.HTML(http.StatusOK, "settings/index.html", common.BuildPageData(c, gin.H{
data := gin.H{
"Title": "个人设置",
"ExtraCSS": "/static/css/settings.css",
"ActiveTab": tab,
@ -78,7 +80,34 @@ func (sc *SettingsController) SettingsPage(c *gin.Context) {
"StatusName": model.StatusDisplayNames[user.Status],
"PendingTypes": pendingTypes,
"AuditTypes": model.AuditTypeNames,
}))
}
// 登录管理 tab 需要额外数据
if tab == "sessions" {
sessions, _ := sc.sessionMgr.ListByUID(uid)
if sessions == nil {
sessions = []*session.Session{}
}
currentSID, _ := c.Cookie(common.SessionCookieName)
// 解析每个 session 的设备信息
type sessionView struct {
*session.Session
DeviceInfo session.DeviceInfo
IsCurrent bool
}
var views []sessionView
for _, s := range sessions {
views = append(views, sessionView{
Session: s,
DeviceInfo: session.ParseUA(s.UserAgent),
IsCurrent: s.ID == currentSID,
})
}
data["Sessions"] = views
data["CurrentSID"] = currentSID
}
c.HTML(http.StatusOK, "settings/index.html", common.BuildPageData(c, data))
}
// AuditStatus 查询当前用户的待审核类型(需登录)