From 4b1b8b22570a4abdb69614c42f4e7af52a7eb211 Mon Sep 17 00:00:00 2001 From: okatu-loli Date: Sat, 23 Aug 2025 18:54:14 +0800 Subject: [PATCH] feat(session): Adds session management features - Added `SessionInactive` error type in `device.go` - Added session-related APIs in `router.go` to support listing and evicting sessions - Added `ListSessionsByUser`, `ListSessions`, and `MarkInactive` methods in `session.go` - Returns an appropriate error when the session state is `SessionInactive` --- internal/db/session.go | 16 ++++++++++ internal/device/session.go | 3 ++ internal/errs/device.go | 3 +- server/handles/session.go | 62 ++++++++++++++++++++++++++++++++++++++ server/router.go | 6 ++++ 5 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 server/handles/session.go diff --git a/internal/db/session.go b/internal/db/session.go index 8b0f7333..ef35d3bd 100644 --- a/internal/db/session.go +++ b/internal/db/session.go @@ -47,3 +47,19 @@ func GetOldestSession(userID uint) (*model.Session, error) { func UpdateSessionLastActive(userID uint, deviceKey string, lastActive int64) error { return errors.WithStack(db.Model(&model.Session{}).Where("user_id = ? AND device_key = ?", userID, deviceKey).Update("last_active", lastActive).Error) } + +func ListSessionsByUser(userID uint) ([]model.Session, error) { + var sessions []model.Session + err := db.Where("user_id = ? AND status = ?", userID, model.SessionActive).Find(&sessions).Error + return sessions, errors.WithStack(err) +} + +func ListSessions() ([]model.Session, error) { + var sessions []model.Session + err := db.Where("status = ?", model.SessionActive).Find(&sessions).Error + return sessions, errors.WithStack(err) +} + +func MarkInactive(sessionID string) error { + return errors.WithStack(db.Model(&model.Session{}).Where("device_key = ?", sessionID).Update("status", model.SessionInactive).Error) +} diff --git a/internal/device/session.go b/internal/device/session.go index f608e735..d4f0172c 100644 --- a/internal/device/session.go +++ b/internal/device/session.go @@ -22,6 +22,9 @@ func Handle(userID uint, deviceKey string) error { now := time.Now().Unix() sess, err := db.GetSession(userID, deviceKey) if err == nil { + if sess.Status == model.SessionInactive { + return errors.WithStack(errs.SessionInactive) + } sess.LastActive = now sess.Status = model.SessionActive return db.UpsertSession(sess) diff --git a/internal/errs/device.go b/internal/errs/device.go index 9d3bd744..3b79298a 100644 --- a/internal/errs/device.go +++ b/internal/errs/device.go @@ -3,5 +3,6 @@ package errs import "errors" var ( - TooManyDevices = errors.New("too many active devices") + TooManyDevices = errors.New("too many active devices") + SessionInactive = errors.New("session inactive") ) diff --git a/server/handles/session.go b/server/handles/session.go new file mode 100644 index 00000000..c3dd833e --- /dev/null +++ b/server/handles/session.go @@ -0,0 +1,62 @@ +package handles + +import ( + "github.com/alist-org/alist/v3/internal/db" + "github.com/alist-org/alist/v3/internal/model" + "github.com/alist-org/alist/v3/server/common" + "github.com/gin-gonic/gin" +) + +func ListMySessions(c *gin.Context) { + user := c.MustGet("user").(*model.User) + sessions, err := db.ListSessionsByUser(user.ID) + if err != nil { + common.ErrorResp(c, err, 500) + return + } + common.SuccessResp(c, sessions) +} + +type EvictSessionReq struct { + SessionID string `json:"session_id"` +} + +func EvictMySession(c *gin.Context) { + var req EvictSessionReq + if err := c.ShouldBindJSON(&req); err != nil { + common.ErrorResp(c, err, 400) + return + } + user := c.MustGet("user").(*model.User) + if _, err := db.GetSession(user.ID, req.SessionID); err != nil { + common.ErrorResp(c, err, 400) + return + } + if err := db.MarkInactive(req.SessionID); err != nil { + common.ErrorResp(c, err, 500) + return + } + common.SuccessResp(c) +} + +func ListSessions(c *gin.Context) { + sessions, err := db.ListSessions() + if err != nil { + common.ErrorResp(c, err, 500) + return + } + common.SuccessResp(c, sessions) +} + +func EvictSession(c *gin.Context) { + var req EvictSessionReq + if err := c.ShouldBindJSON(&req); err != nil { + common.ErrorResp(c, err, 400) + return + } + if err := db.MarkInactive(req.SessionID); err != nil { + common.ErrorResp(c, err, 500) + return + } + common.SuccessResp(c) +} diff --git a/server/router.go b/server/router.go index bb239ed0..4d79c1fd 100644 --- a/server/router.go +++ b/server/router.go @@ -71,6 +71,8 @@ func Init(e *gin.Engine) { auth.POST("/auth/2fa/generate", handles.Generate2FA) auth.POST("/auth/2fa/verify", handles.Verify2FA) auth.GET("/auth/logout", handles.LogOut) + auth.GET("/me/sessions", handles.ListMySessions) + auth.POST("/me/sessions/evict", handles.EvictMySession) // auth api.GET("/auth/sso", handles.SSOLoginRedirect) @@ -185,6 +187,10 @@ func admin(g *gin.RouterGroup) { labelFileBinding.POST("/delete", handles.DelLabelByFileName) labelFileBinding.POST("/restore", handles.RestoreLabelFileBinding) + session := g.Group("/session") + session.GET("/list", handles.ListSessions) + session.POST("/evict", handles.EvictSession) + } func _fs(g *gin.RouterGroup) {