alist/server/handles/user.go
千石 eb4c35db75
feat(device/session): per-user max devices & TTL, WebDAV reactivation, admin clean/list APIs (#9315)
* 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.

* feat(session): Added session cleanup functionality

- Added the `/clean` route to the route for session cleanup
- Added the `DeleteInactiveSessions` method to support deleting inactive sessions by user ID
- Added the `DeleteSessionByID` method to delete a specific session by session ID
- Defined the `CleanSessionsReq` request structure to support passing a user ID or session ID
- Implemented the `CleanSessions` interface logic to perform corresponding session cleanup operations based on the request parameters

* feat(session): Added session list functionality with usernames

- Added the `SessionWithUser` structure, which includes `Session` and `Username` fields.
- Added the `ListSessionsWithUser` function, which queries and returns a list of sessions with usernames.
- Used a `JOIN` operation to join the session and user tables to retrieve the username associated with each session.
- Changed `ListSessions` to `ListSessionsWithUser` to ensure that the username is retrieved.

* feat(webdav): Enhanced WebDAV authentication logic

- Added logic for generating device keys based on the Client-Id, prioritizing those obtained from the request header.
- If the Client-Id is missing, attempts to obtain it from the cookie. If that still doesn't exist, generates a random suffix for the client IP address as an identifier.
- Stores the generated Client-Id in a cookie to ensure consistency across subsequent requests.
- Use the device.EnsureActiveOnLogin method instead of the original Handle method to reactivate inactive sessions.
2025-09-11 11:27:07 +08:00

174 lines
3.6 KiB
Go

package handles
import (
"strconv"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/server/common"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
)
func ListUsers(c *gin.Context) {
var req model.PageReq
if err := c.ShouldBind(&req); err != nil {
common.ErrorResp(c, err, 400)
return
}
req.Validate()
log.Debugf("%+v", req)
users, total, err := op.GetUsers(req.Page, req.PerPage)
if err != nil {
common.ErrorResp(c, err, 500, true)
return
}
common.SuccessResp(c, common.PageResp{
Content: users,
Total: total,
})
}
func CreateUser(c *gin.Context) {
var req model.User
if err := c.ShouldBind(&req); err != nil {
common.ErrorResp(c, err, 400)
return
}
if len(req.Role) == 0 {
req.Role = model.Roles{op.GetDefaultRoleID()}
}
if req.IsAdmin() || req.IsGuest() {
common.ErrorStrResp(c, "admin or guest user can not be created", 400, true)
return
}
req.SetPassword(req.Password)
req.Password = ""
req.Authn = "[]"
if err := op.CreateUser(&req); err != nil {
common.ErrorResp(c, err, 500, true)
} else {
common.SuccessResp(c)
}
}
func UpdateUser(c *gin.Context) {
var req model.User
if err := c.ShouldBind(&req); err != nil {
common.ErrorResp(c, err, 400)
return
}
user, err := op.GetUserById(req.ID)
if err != nil {
common.ErrorResp(c, err, 500)
return
}
if user.Username == "admin" {
if !utils.SliceEqual(user.Role, req.Role) {
common.ErrorStrResp(c, "cannot change role of admin user", 403)
return
}
//if user.Username != req.Username {
// common.ErrorStrResp(c, "cannot change username of admin user", 403)
// return
//}
}
if req.Password == "" {
req.PwdHash = user.PwdHash
req.Salt = user.Salt
} else {
req.SetPassword(req.Password)
req.Password = ""
}
if req.OtpSecret == "" {
req.OtpSecret = user.OtpSecret
}
if req.MaxDevices == nil {
req.MaxDevices = user.MaxDevices
}
if req.SessionTTL == nil {
req.SessionTTL = user.SessionTTL
}
if req.Disabled && user.IsAdmin() {
count, err := op.CountEnabledAdminsExcluding(user.ID)
if err != nil {
common.ErrorResp(c, err, 500)
return
}
if count == 0 {
common.ErrorStrResp(c, "at least one enabled admin must be kept", 400)
return
}
}
if !utils.SliceEqual(user.Role, req.Role) {
if req.IsAdmin() || req.IsGuest() {
common.ErrorStrResp(c, "cannot assign admin or guest role to user", 400, true)
return
}
}
if err := op.UpdateUser(&req); err != nil {
common.ErrorResp(c, err, 500)
} else {
common.SuccessResp(c)
}
}
func DeleteUser(c *gin.Context) {
idStr := c.Query("id")
id, err := strconv.Atoi(idStr)
if err != nil {
common.ErrorResp(c, err, 400)
return
}
if err := op.DeleteUserById(uint(id)); err != nil {
common.ErrorResp(c, err, 500)
return
}
common.SuccessResp(c)
}
func GetUser(c *gin.Context) {
idStr := c.Query("id")
id, err := strconv.Atoi(idStr)
if err != nil {
common.ErrorResp(c, err, 400)
return
}
user, err := op.GetUserById(uint(id))
if err != nil {
common.ErrorResp(c, err, 500, true)
return
}
common.SuccessResp(c, user)
}
func Cancel2FAById(c *gin.Context) {
idStr := c.Query("id")
id, err := strconv.Atoi(idStr)
if err != nil {
common.ErrorResp(c, err, 400)
return
}
if err := op.Cancel2FAById(uint(id)); err != nil {
common.ErrorResp(c, err, 500)
return
}
common.SuccessResp(c)
}
func DelUserCache(c *gin.Context) {
username := c.Query("username")
err := op.DelUserCache(username)
if err != nil {
common.ErrorResp(c, err, 500)
return
}
common.SuccessResp(c)
}