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:
161
internal/controller/settings_api_sessions.go
Normal file
161
internal/controller/settings_api_sessions.go
Normal file
@ -0,0 +1,161 @@
|
||||
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, "备注已更新")
|
||||
}
|
||||
Reference in New Issue
Block a user