alist/internal/device/session.go
okatu-loli 2d5b8a9a61 feat(auth): Improved device session management logic
- Replaced the `userID` parameter with the `user` object to support operations with more user attributes.
- Introduced `SessionTTL` and `MaxDevices` properties in the `Handle` and `EnsureActiveOnLogin` functions to support user-defined settings.
- Adjusted the session creation and verification logic in `session.go` to support user-defined device count and session duration.
- Added help documentation in `setting.go` to explain the configuration purposes of `MaxDevices` and `DeviceSessionTTL`.
- Added optional `MaxDevices` and `SessionTTL` properties to the user entity in `user.go` and persisted these settings across user updates.
- Modified the device handling logic in `webdav.go` to adapt to the new user object parameters.
2025-09-09 21:45:46 +08:00

158 lines
4.1 KiB
Go

package device
import (
"time"
"github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/internal/db"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/setting"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/pkg/errors"
"gorm.io/gorm"
)
// Handle verifies device sessions for a user and upserts current session.
func Handle(user *model.User, deviceKey, ua, ip string) error {
ttl := setting.GetInt(conf.DeviceSessionTTL, 86400)
if user.SessionTTL != nil {
ttl = *user.SessionTTL
}
if ttl > 0 {
_ = db.DeleteSessionsBefore(time.Now().Unix() - int64(ttl))
}
ip = utils.MaskIP(ip)
now := time.Now().Unix()
sess, err := db.GetSession(user.ID, deviceKey)
if err == nil {
if sess.Status == model.SessionInactive {
return errors.WithStack(errs.SessionInactive)
}
sess.Status = model.SessionActive
sess.LastActive = now
sess.UserAgent = ua
sess.IP = ip
return db.UpsertSession(sess)
}
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
max := setting.GetInt(conf.MaxDevices, 0)
if user.MaxDevices != nil {
max = *user.MaxDevices
}
if max > 0 {
count, err := db.CountActiveSessionsByUser(user.ID)
if err != nil {
return err
}
if count >= int64(max) {
policy := setting.GetStr(conf.DeviceEvictPolicy, "deny")
if policy == "evict_oldest" {
if oldest, err := db.GetOldestActiveSession(user.ID); err == nil {
if err := db.MarkInactive(oldest.DeviceKey); err != nil {
return err
}
}
} else {
return errors.WithStack(errs.TooManyDevices)
}
}
}
s := &model.Session{UserID: user.ID, DeviceKey: deviceKey, UserAgent: ua, IP: ip, LastActive: now, Status: model.SessionActive}
return db.CreateSession(s)
}
// EnsureActiveOnLogin is used only in login flow:
// - If session exists (even Inactive): reactivate and refresh fields.
// - If not exists: apply max-devices policy, then create Active session.
func EnsureActiveOnLogin(user *model.User, deviceKey, ua, ip string) error {
ttl := setting.GetInt(conf.DeviceSessionTTL, 86400)
if user.SessionTTL != nil {
ttl = *user.SessionTTL
}
if ttl > 0 {
_ = db.DeleteSessionsBefore(time.Now().Unix() - int64(ttl))
}
ip = utils.MaskIP(ip)
now := time.Now().Unix()
sess, err := db.GetSession(user.ID, deviceKey)
if err == nil {
if sess.Status == model.SessionInactive {
max := setting.GetInt(conf.MaxDevices, 0)
if user.MaxDevices != nil {
max = *user.MaxDevices
}
if max > 0 {
count, err := db.CountActiveSessionsByUser(user.ID)
if err != nil {
return err
}
if count >= int64(max) {
policy := setting.GetStr(conf.DeviceEvictPolicy, "deny")
if policy == "evict_oldest" {
if oldest, gerr := db.GetOldestActiveSession(user.ID); gerr == nil {
if err := db.MarkInactive(oldest.DeviceKey); err != nil {
return err
}
}
} else {
return errors.WithStack(errs.TooManyDevices)
}
}
}
}
sess.Status = model.SessionActive
sess.LastActive = now
sess.UserAgent = ua
sess.IP = ip
return db.UpsertSession(sess)
}
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
max := setting.GetInt(conf.MaxDevices, 0)
if user.MaxDevices != nil {
max = *user.MaxDevices
}
if max > 0 {
count, err := db.CountActiveSessionsByUser(user.ID)
if err != nil {
return err
}
if count >= int64(max) {
policy := setting.GetStr(conf.DeviceEvictPolicy, "deny")
if policy == "evict_oldest" {
if oldest, gerr := db.GetOldestActiveSession(user.ID); gerr == nil {
if err := db.MarkInactive(oldest.DeviceKey); err != nil {
return err
}
}
} else {
return errors.WithStack(errs.TooManyDevices)
}
}
}
return db.CreateSession(&model.Session{
UserID: user.ID,
DeviceKey: deviceKey,
UserAgent: ua,
IP: ip,
LastActive: now,
Status: model.SessionActive,
})
}
// Refresh updates last_active for the session.
func Refresh(userID uint, deviceKey string) {
_ = db.UpdateSessionLastActive(userID, deviceKey, time.Now().Unix())
}