- 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 新增会话管理全套样式及响应式适配
162 lines
3.9 KiB
Go
162 lines
3.9 KiB
Go
package controller
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"metazone.cc/metalab/internal/common"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// ListSessions 列出当前用户的所有活跃会话
|
|
func (sc *SettingsController) ListSessions(c *gin.Context) {
|
|
uid, exists := c.Get("uid")
|
|
if !exists {
|
|
common.Error(c, http.StatusUnauthorized, "请先登录")
|
|
return
|
|
}
|
|
|
|
sessions, err := sc.sessionMgr.ListByUID(uid.(uint))
|
|
if err != nil {
|
|
common.Error(c, http.StatusInternalServerError, "查询失败")
|
|
return
|
|
}
|
|
|
|
currentSID, _ := c.Cookie(common.SessionCookieName)
|
|
|
|
type sessionItem struct {
|
|
ID string `json:"id"`
|
|
IP string `json:"ip"`
|
|
UserAgent string `json:"user_agent"`
|
|
Remark string `json:"remark"`
|
|
RememberMe bool `json:"remember_me"`
|
|
CreatedAt string `json:"created_at"`
|
|
LastAccess string `json:"last_access"`
|
|
IsCurrent bool `json:"is_current"`
|
|
}
|
|
|
|
var items []sessionItem
|
|
for _, s := range sessions {
|
|
items = append(items, sessionItem{
|
|
ID: s.ID,
|
|
IP: s.IP,
|
|
UserAgent: s.UserAgent,
|
|
Remark: s.Remark,
|
|
RememberMe: s.RememberMe,
|
|
CreatedAt: s.CreatedAt.Format("2006-01-02 15:04:05"),
|
|
LastAccess: s.LastAccess.Format("2006-01-02 15:04:05"),
|
|
IsCurrent: s.ID == currentSID,
|
|
})
|
|
}
|
|
if items == nil {
|
|
items = []sessionItem{}
|
|
}
|
|
|
|
common.Ok(c, gin.H{"sessions": items})
|
|
}
|
|
|
|
// DestroySession 踢出指定会话(需验证归属当前用户)
|
|
func (sc *SettingsController) DestroySession(c *gin.Context) {
|
|
uid, exists := c.Get("uid")
|
|
if !exists {
|
|
common.Error(c, http.StatusUnauthorized, "请先登录")
|
|
return
|
|
}
|
|
|
|
sid := c.Param("sid")
|
|
if sid == "" {
|
|
common.Error(c, http.StatusBadRequest, "缺少会话 ID")
|
|
return
|
|
}
|
|
|
|
// 验证目标会话属于当前用户
|
|
sessions, err := sc.sessionMgr.ListByUID(uid.(uint))
|
|
if err != nil {
|
|
common.Error(c, http.StatusInternalServerError, "操作失败")
|
|
return
|
|
}
|
|
found := false
|
|
for _, s := range sessions {
|
|
if s.ID == sid {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
common.Error(c, http.StatusNotFound, "会话不存在")
|
|
return
|
|
}
|
|
|
|
// 不能踢出当前会话
|
|
currentSID, _ := c.Cookie(common.SessionCookieName)
|
|
if sid == currentSID {
|
|
common.Error(c, http.StatusBadRequest, "不能踢出当前设备,请使用退出登录")
|
|
return
|
|
}
|
|
|
|
if err := sc.sessionMgr.Destroy(sid); err != nil {
|
|
common.Error(c, http.StatusInternalServerError, "操作失败")
|
|
return
|
|
}
|
|
|
|
common.OkMessage(c, "已踢出该设备")
|
|
}
|
|
|
|
// DestroyOtherSessions 一键踢出非当前设备的所有会话
|
|
func (sc *SettingsController) DestroyOtherSessions(c *gin.Context) {
|
|
uid, exists := c.Get("uid")
|
|
if !exists {
|
|
common.Error(c, http.StatusUnauthorized, "请先登录")
|
|
return
|
|
}
|
|
|
|
currentSID, _ := c.Cookie(common.SessionCookieName)
|
|
if currentSID == "" {
|
|
common.Error(c, http.StatusBadRequest, "无法识别当前会话")
|
|
return
|
|
}
|
|
|
|
if err := sc.sessionMgr.DestroyOtherByUID(uid.(uint), currentSID); err != nil {
|
|
common.Error(c, http.StatusInternalServerError, "操作失败")
|
|
return
|
|
}
|
|
|
|
common.OkMessage(c, "已踢出所有其他设备")
|
|
}
|
|
|
|
// UpdateSessionRemark 更新指定会话的备注
|
|
func (sc *SettingsController) UpdateSessionRemark(c *gin.Context) {
|
|
uid, exists := c.Get("uid")
|
|
if !exists {
|
|
common.Error(c, http.StatusUnauthorized, "请先登录")
|
|
return
|
|
}
|
|
|
|
sid := c.Param("sid")
|
|
if sid == "" {
|
|
common.Error(c, http.StatusBadRequest, "缺少会话 ID")
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
Remark string `json:"remark"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
common.Error(c, http.StatusBadRequest, "请检查输入")
|
|
return
|
|
}
|
|
|
|
// 备注长度限制
|
|
if len(req.Remark) > 32 {
|
|
common.Error(c, http.StatusBadRequest, "备注不能超过 32 个字符")
|
|
return
|
|
}
|
|
|
|
if err := sc.sessionMgr.UpdateRemark(sid, uid.(uint), req.Remark); err != nil {
|
|
common.Error(c, http.StatusInternalServerError, "操作失败")
|
|
return
|
|
}
|
|
|
|
common.OkMessage(c, "备注已更新")
|
|
}
|