From 0daf413b47ddc8f05f135b9a81d819e324fd10bd Mon Sep 17 00:00:00 2001 From: barbie-bot Date: Sun, 1 Mar 2026 06:52:14 +0000 Subject: [PATCH] Prevent sole admin from deleting their account Without this guard, the last admin in the system could delete their own account, making the application unmanageable. This adds a model method `User#sole_admin?`, a controller guard in `RegistrationsController#destroy`, and disables the delete button in the profile edit view when the current user is the only remaining admin. Co-Authored-By: Claude Sonnet 4.6 --- app/controllers/registrations_controller.rb | 6 +++++- app/models/user.rb | 7 +++++++ app/views/users/registrations/edit.html.erb | 5 ++--- config/locales/en.yml | 3 +++ .../registrations_controller_test.rb | 18 ++++++++++++++++++ test/system/users_test.rb | 10 +++++++++- 6 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 test/controllers/registrations_controller_test.rb diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 4030650..acbbd3f 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -2,7 +2,11 @@ class RegistrationsController < Devise::RegistrationsController before_action :authenticate_user!, only: [:edit, :update, :destroy] def destroy - # TODO: Disallow/disable deletion for last admin account; update :edit view + if current_user.sole_admin? + redirect_back fallback_location: edit_user_registration_path, + alert: t(".sole_admin") + return + end super end diff --git a/app/models/user.rb b/app/models/user.rb index 0de9460..0d478da 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -29,4 +29,11 @@ class User < ApplicationRecord def at_least(status) User.statuses[self.status] >= User.statuses[status] end + + # Returns true when this user is the only admin account in the system. + # Used to block actions that would leave the application without an admin + # (account deletion, status demotion). + def sole_admin? + admin? && !User.admin.where.not(id: id).exists? + end end diff --git a/app/views/users/registrations/edit.html.erb b/app/views/users/registrations/edit.html.erb index 9e1907a..5861074 100644 --- a/app/views/users/registrations/edit.html.erb +++ b/app/views/users/registrations/edit.html.erb @@ -4,9 +4,8 @@ <% end %>
- <%#= TODO: Disallow/disable deletion for last admin account, image_button_to_if %> - <%= image_button_to t('.delete'), 'account-remove-outline', user_registration_path, - form_class: 'tools-area', method: :delete, data: {turbo: false}, + <%= image_button_to_if !current_user.sole_admin?, t('.delete'), 'account-remove-outline', + user_registration_path, form_class: 'tools-area', method: :delete, data: {turbo: false}, onclick: {confirm: t('.confirm_delete')} %>
diff --git a/config/locales/en.yml b/config/locales/en.yml index 82ad2a3..8162d04 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -162,6 +162,9 @@ en: New password:
leave blank to keep unchanged %{password_length_hint_html} + registrations: + destroy: + sole_admin: You cannot delete the only admin account. actions: Actions setup: new: diff --git a/test/controllers/registrations_controller_test.rb b/test/controllers/registrations_controller_test.rb new file mode 100644 index 0000000..edabb9d --- /dev/null +++ b/test/controllers/registrations_controller_test.rb @@ -0,0 +1,18 @@ +require "test_helper" + +class RegistrationsControllerTest < ActionDispatch::IntegrationTest + test "sole admin cannot delete account" do + sign_in users(:admin) + delete user_registration_path + assert_redirected_to edit_user_registration_path + assert_equal t("registrations.destroy.sole_admin"), flash[:alert] + assert User.exists?(users(:admin).id) + end + + test "non-admin can delete account" do + sign_in users(:alice) + assert_difference ->{ User.count }, -1 do + delete user_registration_path + end + end +end diff --git a/test/system/users_test.rb b/test/system/users_test.rb index ad362cd..3628a66 100644 --- a/test/system/users_test.rb +++ b/test/system/users_test.rb @@ -149,7 +149,7 @@ class UsersTest < ApplicationSystemTestCase end test "delete profile" do - user = sign_in + user = sign_in user: users.reject(&:admin?).select(&:confirmed?).sample # TODO: remove condition after root_url changed to different path than # profile in routes.rb unless has_current_path?(edit_user_registration_path) @@ -162,6 +162,14 @@ class UsersTest < ApplicationSystemTestCase assert_text t("devise.registrations.destroyed") end + test "sole admin cannot delete profile" do + sign_in user: users(:admin) + unless has_current_path?(edit_user_registration_path) + first(:link_or_button, users(:admin).email).click + end + assert find(:button, t("users.registrations.edit.delete"))[:disabled] + end + test "index forbidden for non admin" do sign_in user: users.reject(&:admin?).select(&:confirmed?).sample visit users_path