From f04af0cac6c808b8e7c9a9651380c252c4de9132 Mon Sep 17 00:00:00 2001 From: Ariel Leyva Date: Wed, 11 Mar 2026 14:36:05 -0400 Subject: [PATCH] fix: allow deleting the user's own account (#5820) --- errors/errors.go | 2 +- frontend/src/views/settings/User.vue | 7 ++++++- storage/bolt/users.go | 27 +++++++++++++++++++++++++++ users/storage.go | 23 +++++++++++++++++++++-- 4 files changed, 55 insertions(+), 4 deletions(-) diff --git a/errors/errors.go b/errors/errors.go index 748354a8..f6b86caf 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -21,7 +21,7 @@ var ( ErrPermissionDenied = errors.New("permission denied") ErrInvalidRequestParams = errors.New("invalid request params") ErrSourceIsParent = errors.New("source is parent") - ErrRootUserDeletion = errors.New("user with id 1 can't be deleted") + ErrRootUserDeletion = errors.New("the sole admin can't be deleted") ErrCurrentPasswordIncorrect = errors.New("the current password is incorrect") ) diff --git a/frontend/src/views/settings/User.vue b/frontend/src/views/settings/User.vue index b726d9ef..63e0d2a2 100644 --- a/frontend/src/views/settings/User.vue +++ b/frontend/src/views/settings/User.vue @@ -59,6 +59,7 @@ import { useRoute, useRouter } from "vue-router"; import { useI18n } from "vue-i18n"; import { StatusError } from "@/api/utils"; import { authMethod } from "@/utils/constants"; +import { logout } from "@/utils/auth"; const error = ref(); const originalUser = ref(); @@ -144,7 +145,11 @@ const deleteUser = async (currentPassword: string) => { } try { await api.remove(user.value.id, currentPassword); - router.push({ path: "/settings/users" }); + if (user.value.id == authStore.user?.id) { + logout(); + } else { + router.push({ path: "/settings/users" }); + } $showSuccess(t("settings.userDeleted")); } catch (err) { if (err instanceof StatusError) { diff --git a/storage/bolt/users.go b/storage/bolt/users.go index 974c0a48..6686d941 100644 --- a/storage/bolt/users.go +++ b/storage/bolt/users.go @@ -6,6 +6,7 @@ import ( "reflect" "github.com/asdine/storm/v3" + bolt "go.etcd.io/bbolt" fberrors "github.com/filebrowser/filebrowser/v2/errors" "github.com/filebrowser/filebrowser/v2/users" @@ -93,3 +94,29 @@ func (st usersBackend) DeleteByUsername(username string) error { return st.db.DeleteStruct(user) } + +func (st usersBackend) CountAdmins() (int, error) { + count := 0 + + err := st.db.Bolt.View(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(reflect.TypeOf(users.User{}).Name())) + if bucket == nil { + return nil + } + + c := bucket.Cursor() + for _, v := c.First(); v != nil; _, v = c.Next() { + var u users.User + if err := st.db.Codec().Unmarshal(v, &u); err != nil { + return err + } + if u.Perm.Admin { + count++ + } + } + + return nil + }) + + return count, err +} diff --git a/users/storage.go b/users/storage.go index 43c65fe4..33cfc9c4 100644 --- a/users/storage.go +++ b/users/storage.go @@ -15,6 +15,7 @@ type StorageBackend interface { Update(u *User, fields ...string) error DeleteByID(uint) error DeleteByUsername(string) error + CountAdmins() (int, error) } type Store interface { @@ -108,14 +109,20 @@ func (s *Storage) Delete(id interface{}) error { if err != nil { return err } - if user.ID == 1 { + if s.IsUniqueAdmin(user) { return fberrors.ErrRootUserDeletion } + return s.back.DeleteByUsername(id) case uint: - if id == 1 { + user, err := s.back.GetBy(id) + if err != nil { + return err + } + if s.IsUniqueAdmin(user) { return fberrors.ErrRootUserDeletion } + return s.back.DeleteByID(id) default: return fberrors.ErrInvalidDataType @@ -131,3 +138,15 @@ func (s *Storage) LastUpdate(id uint) int64 { } return 0 } + +func (s *Storage) IsUniqueAdmin(user *User) bool { + if !user.Perm.Admin { + return false + } + + count, err := s.back.CountAdmins() + if err != nil { + return true + } + return count <= 1 +}