From 5b8c26510b720a2cd308023dc7cd1c8bd1e9d20c Mon Sep 17 00:00:00 2001 From: qianshi Date: Mon, 28 Jul 2025 23:07:07 +0800 Subject: [PATCH] feat(user-management): Enhance admin management and role handling - Add `CountEnabledAdminsExcluding` function to count enabled admins excluding a specific user. - Implement `CountUsersByRoleAndEnabledExclude` in `internal/db/user.go` to support exclusion logic. - Refactor role handling with switch-case for better readability in `server/handles/role.go`. - Ensure at least one enabled admin remains when disabling an admin in `server/handles/user.go`. - Maintain guest role name consistency when updating roles in `internal/op/role.go`. --- internal/db/user.go | 11 +++++++++++ internal/op/role.go | 6 +++++- internal/op/user.go | 8 ++++++++ server/handles/role.go | 6 +++++- server/handles/user.go | 13 ++++++++++--- 5 files changed, 39 insertions(+), 5 deletions(-) diff --git a/internal/db/user.go b/internal/db/user.go index f2b6635a..9e8ee3fc 100644 --- a/internal/db/user.go +++ b/internal/db/user.go @@ -2,6 +2,7 @@ package db import ( "encoding/base64" + "fmt" "github.com/alist-org/alist/v3/internal/model" "github.com/alist-org/alist/v3/pkg/utils" "github.com/go-webauthn/webauthn/webauthn" @@ -140,3 +141,13 @@ func UpdateUserBasePathPrefix(oldPath, newPath string) ([]string, error) { return modifiedUsernames, nil } + +func CountUsersByRoleAndEnabledExclude(roleID uint, excludeUserID uint) (int64, error) { + var count int64 + jsonValue := fmt.Sprintf("[%d]", roleID) + err := db.Model(&model.User{}). + Where("disabled = ? AND id != ?", false, excludeUserID). + Where("JSON_CONTAINS(role, ?)", jsonValue). + Count(&count).Error + return count, err +} diff --git a/internal/op/role.go b/internal/op/role.go index 64502f98..b7474566 100644 --- a/internal/op/role.go +++ b/internal/op/role.go @@ -96,8 +96,12 @@ func UpdateRole(r *model.Role) error { if err != nil { return err } - if old.Name == "admin" || old.Name == "guest" { + switch old.Name { + case "admin": return errs.ErrChangeDefaultRole + + case "guest": + r.Name = "guest" } for i := range r.PermissionScopes { r.PermissionScopes[i].Path = utils.FixAndCleanPath(r.PermissionScopes[i].Path) diff --git a/internal/op/user.go b/internal/op/user.go index e775df63..b9662015 100644 --- a/internal/op/user.go +++ b/internal/op/user.go @@ -136,3 +136,11 @@ func DelUserCache(username string) error { userCache.Del(username) return nil } + +func CountEnabledAdminsExcluding(userID uint) (int64, error) { + adminRole, err := GetRoleByName("admin") + if err != nil { + return 0, err + } + return db.CountUsersByRoleAndEnabledExclude(adminRole.ID, userID) +} diff --git a/server/handles/role.go b/server/handles/role.go index 1bf7d499..0d071c9f 100644 --- a/server/handles/role.go +++ b/server/handles/role.go @@ -66,9 +66,13 @@ func UpdateRole(c *gin.Context) { common.ErrorResp(c, err, 500, true) return } - if role.Name == "admin" || role.Name == "guest" { + switch role.Name { + case "admin": common.ErrorResp(c, errs.ErrChangeDefaultRole, 403) return + + case "guest": + req.Name = "guest" } if err := op.UpdateRole(&req); err != nil { common.ErrorResp(c, err, 500, true) diff --git a/server/handles/user.go b/server/handles/user.go index 50eaf969..858c2a3b 100644 --- a/server/handles/user.go +++ b/server/handles/user.go @@ -74,9 +74,16 @@ func UpdateUser(c *gin.Context) { if req.OtpSecret == "" { req.OtpSecret = user.OtpSecret } - if req.Disabled && req.IsAdmin() { - common.ErrorStrResp(c, "admin user can not be disabled", 400) - return + 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 err := op.UpdateUser(&req); err != nil { common.ErrorResp(c, err, 500)