This repository has been archived on 2026-06-21. You can view files and clone it, but cannot push or open issues or pull requests.
Files
MetaLab/internal/session/manager.go
Victor_Jay d203447eb3 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 新增会话管理全套样式及响应式适配
2026-05-30 23:58:37 +08:00

130 lines
3.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package session
import (
"log"
"time"
"metazone.cc/metalab/internal/config"
"metazone.cc/metalab/internal/model"
)
// userStore SessionManager 对 UserRepo 的最小接口ISP1 个方法)
type userStore interface {
FindByIDForAuth(userID uint) (*model.User, error)
}
// Manager 会话管理器:创建、验证、销毁、续期
type Manager struct {
store Store
userRepo userStore
cfg *config.Config
}
// NewManager 构造函数
func NewManager(store Store, userRepo userStore, cfg *config.Config) *Manager {
return &Manager{store: store, userRepo: userRepo, cfg: cfg}
}
// Create 创建会话并写入存储
func (m *Manager) Create(user *model.User, rememberMe bool, ip, userAgent string) (string, error) {
sid, err := NewID()
if err != nil {
return "", err
}
now := time.Now()
s := &Session{
ID: sid,
UserID: user.ID,
Email: user.Email,
Username: user.Username,
Role: user.Role,
RememberMe: rememberMe,
IP: ip,
UserAgent: userAgent,
CreatedAt: now,
LastAccess: now,
}
if err := m.store.Set(s); err != nil {
return "", err
}
log.Printf("[SessionManager] Created sid=%s uid=%d rememberMe=%v ip=%s", sid[:16]+"...", user.ID, rememberMe, ip)
return sid, nil
}
// Validate 验证会话:读取 → 检查过期 → 检查用户状态 → 续期 → 返回
// 返回 nil 表示会话无效(不存在/过期/用户被封禁)
func (m *Manager) Validate(sid string) (*Session, error) {
if sid == "" {
return nil, nil
}
s, err := m.store.Get(sid)
if err != nil || s == nil {
return nil, nil
}
// 检查用户是否仍然有效(封禁/注销后拒绝已有会话)
user, err := m.userRepo.FindByIDForAuth(s.UserID)
if err != nil {
log.Printf("[SessionManager] Validate: user not found uid=%d err=%v", s.UserID, err)
m.store.Delete(sid)
return nil, nil
}
// 封禁用户直接拒绝
if user.Status == model.StatusBanned {
log.Printf("[SessionManager] Validate: user banned uid=%d", s.UserID)
m.store.Delete(sid)
return nil, nil
}
// 滑动窗口续期
s.Touch()
if err := m.store.Set(s); err != nil {
log.Printf("[SessionManager] Validate: touch failed sid=%s err=%v", sid[:16]+"...", err)
}
return s, nil
}
// Destroy 销毁单个会话(用户主动退出登录)
func (m *Manager) Destroy(sid string) error {
return m.store.Delete(sid)
}
// DestroyByUID 销毁某用户的所有会话(改密/注销/强制下线)
func (m *Manager) DestroyByUID(uid uint) error {
return m.store.DeleteByUID(uid)
}
// IdleTimeout 根据 rememberMe 返回对应的空闲超时时间
func (m *Manager) IdleTimeout(rememberMe bool) time.Duration {
if rememberMe {
return time.Duration(m.cfg.Session.RememberTimeout) * time.Minute
}
return time.Duration(m.cfg.Session.IdleTimeout) * time.Minute
}
// ListByUID 列出某用户的所有活跃会话
func (m *Manager) ListByUID(uid uint) ([]*Session, error) {
return m.store.ListByUID(uid)
}
// DestroyOtherByUID 销毁某用户除当前会话外的所有会话
func (m *Manager) DestroyOtherByUID(uid uint, currentSID string) error {
return m.store.DeleteByUIDExclude(uid, currentSID)
}
// UpdateRemark 更新指定会话的备注(需验证归属)
func (m *Manager) UpdateRemark(sid string, uid uint, remark string) error {
s, err := m.store.Get(sid)
if err != nil || s == nil {
return err
}
if s.UserID != uid {
return nil
}
s.Remark = remark
return m.store.Set(s)
}