{"record":{"id":"825878e1d2bc4852","repo":"BookStackApp/BookStack","slug":"errors-role-cannot-remove-only-admin","errorCode":null,"errorMessage":"errors.role_cannot_remove_only_admin","messagePattern":"errors\\.role_cannot_remove_only_admin","errorType":"exception","errorClass":"UserUpdateException","httpStatus":null,"severity":"error","filePath":"app/Users/UserRepo.php","lineNumber":312,"sourceCode":"        $adminRole = Role::getSystemRole('admin');\n        if ($adminRole->users()->count() > 1) {\n            return false;\n        }\n\n        return true;\n    }\n\n    /**\n     * Set the assigned user roles via an array of role IDs.\n     *\n     * @throws UserUpdateException\n     */\n    protected function setUserRoles(User $user, array $roles): void\n    {\n        $roles = array_filter(array_values($roles));\n\n        if ($this->demotingLastAdmin($user, $roles)) {\n            throw new UserUpdateException(trans('errors.role_cannot_remove_only_admin'), $user->getEditUrl());\n        }\n\n        $user->roles()->sync($roles);\n    }\n\n    /**\n     * Check if the given user is the last admin and their new roles no longer\n     * contain the admin role.\n     */\n    protected function demotingLastAdmin(User $user, array $newRoles): bool\n    {\n        if ($this->isOnlyAdmin($user)) {\n            $adminRole = Role::getSystemRole('admin');\n            if (!in_array(strval($adminRole->id), $newRoles)) {\n                return true;\n            }\n        }\n","sourceCodeStart":294,"sourceCodeEnd":330,"githubUrl":"https://github.com/BookStackApp/BookStack/blob/18f8469a1c72f8cc8497e9372635e6dea5028071/app/Users/UserRepo.php#L294-L330","documentation":"UserRepo::setUserRoles() throws UserUpdateException when a role update would demote the last admin: demotingLastAdmin() detects that the target user currently holds the admin role, is the only member of it, and the new role array no longer contains it. This prevents locking the instance out of administration; the exception redirects back to the user's edit URL with errors.role_cannot_remove_only_admin.","triggerScenarios":"updateWithoutActivity/createWithoutActivity called with a roles array that omits the admin role for the sole admin user; UI role checkboxes unchecked for the last admin.","commonSituations":"Admin removing their own admin role; bulk role sync via API that replaces role sets wholesale; scripts importing users with role lists that drop the admin grant.","solutions":["Promote a second user to Admin before demoting the last one","Include the admin role id in the roles array for the sole admin, or skip that user in bulk updates","Catch UserUpdateException and redirect with a friendly message instead of a raw error","Check demotingLastAdmin/isOnlyAdmin logic before calling setUserRoles"],"exampleFix":"// before\n$userRepo->setUserRoles($lastAdmin, [$editorRoleId]);\n// after\nif (!$userRepo->isOnlyAdmin($lastAdmin)) {\n    $userRepo->setUserRoles($lastAdmin, [$editorRoleId]);\n}","handlingStrategy":"validation","validationCode":"// Verify the new role set keeps admin coverage\n$adminRoleId = \\BookStack\\Access\\Role::getSystemRole('admin')->id;\n$willLoseAdmin = $user->hasRole($adminRoleId)\n    && !in_array($adminRoleId, $newRoleIds)\n    && \\BookStack\\Access\\Role::getSystemRole('admin')->users()->count() === 1;\nif ($willLoseAdmin) { return back()->withErrors('Cannot remove the only admin.'); }\n$userRepo->setUserRoles($user, $newRoleIds);","typeGuard":null,"tryCatchPattern":"try {\n    $userRepo->setUserRoles($user, $roles);\n} catch (\\BookStack\\Exceptions\\UserUpdateException $e) {\n    return redirect($user->getEditUrl())->with('error', $e->getMessage());\n}","preventionTips":["Promote a second admin before demoting the first","Never replace role arrays wholesale for the sole admin","Count admin-role members in bulk sync logic","Catch UserUpdateException in custom controllers"],"tags":["php","roles","permissions","bookstack"],"backgroundTag":"cannot-demote-last-admin","analyzedSha":"18f8469a1c72f8cc8497e9372635e6dea5028071","analyzedAt":"2026-09-02T19:49:33.068Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-10T02:17:09.455Z"}