From c70e6700c79704d93fcbb94f646bc9e022082598 Mon Sep 17 00:00:00 2001 From: Willian Gustavo Veiga Date: Thu, 20 Jun 2024 21:08:52 -0300 Subject: [PATCH] Add feature require_password_to_destroy --- .../devise/registrations_controller.rb | 31 +++++++++++++---- app/views/devise/registrations/edit.html.erb | 20 +++++++++-- lib/devise.rb | 4 +++ lib/devise/models/registerable.rb | 1 + lib/devise/parameter_sanitizer.rb | 3 +- lib/generators/templates/devise.rb | 3 ++ .../custom_registrations_controller_test.rb | 14 ++++++++ test/generators/views_generator_test.rb | 3 +- test/integration/registerable_test.rb | 33 +++++++++++++++++++ .../custom/registrations_controller.rb | 10 ++++++ 10 files changed, 111 insertions(+), 11 deletions(-) diff --git a/app/controllers/devise/registrations_controller.rb b/app/controllers/devise/registrations_controller.rb index f1292b4d90..29b77ea684 100644 --- a/app/controllers/devise/registrations_controller.rb +++ b/app/controllers/devise/registrations_controller.rb @@ -62,12 +62,12 @@ def update end # DELETE /resource - def destroy - resource.destroy - Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name) - set_flash_message! :notice, :destroyed - yield resource if block_given? - respond_with_navigational(resource){ redirect_to after_sign_out_path_for(resource_name), status: Devise.responder.redirect_status } + def destroy(&block) + if destroy_resource(resource, account_destroy_params) + sign_out_resource(resource_name, resource, &block) + else + render :edit, status: 422 + end end # GET /resource/cancel @@ -94,6 +94,14 @@ def update_resource(resource, params) resource.update_with_password(params) end + def destroy_resource(resource, params) + if resource.class.require_password_to_destroy + resource.destroy_with_password(params) + else + resource.destroy + end + end + # Build a devise resource passing in the session. Useful to move # temporary session data to the newly created user. def build_resource(hash = {}) @@ -141,6 +149,10 @@ def account_update_params devise_parameter_sanitizer.sanitize(:account_update) end + def account_destroy_params + devise_parameter_sanitizer.sanitize(:account_destroy)[:current_password] + end + def translation_scope 'devise.registrations' end @@ -160,6 +172,13 @@ def set_flash_message_for_update(resource, prev_unconfirmed_email) set_flash_message :notice, flash_key end + def sign_out_resource(resource_name, resource, &block) + Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name) + set_flash_message! :notice, :destroyed + yield resource if block_given? + respond_with_navigational(resource){ redirect_to after_sign_out_path_for(resource_name), status: Devise.responder.redirect_status } + end + def sign_in_after_change_password? return true if account_update_params[:password].blank? diff --git a/app/views/devise/registrations/edit.html.erb b/app/views/devise/registrations/edit.html.erb index 19bb019bc7..0276532653 100644 --- a/app/views/devise/registrations/edit.html.erb +++ b/app/views/devise/registrations/edit.html.erb @@ -1,7 +1,7 @@

Edit <%= resource_name.to_s.humanize %>

<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %> - <%= render "devise/shared/error_messages", resource: resource %> + <%= render "devise/shared/error_messages", resource: resource if params[:action] == "update" %>

<%= f.label :email %>

@@ -37,6 +37,22 @@

Cancel my account

-
Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?", turbo_confirm: "Are you sure?" }, method: :delete %>
+<% if resource.class.require_password_to_destroy %> + <%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { name: 'require_password_to_destroy', method: :delete }) do |f| %> + <%= render "devise/shared/error_messages", resource: resource if params[:action] == "destroy" %> + +
+ <%= f.label :current_password, for: :require_password_to_destroy_current_password %> + (we need your current password to delete your account)
+ <%= f.password_field :current_password, id: :require_password_to_destroy_current_password, autocomplete: "off" %> +
+ +
+ <%= f.submit "Cancel my account" %> +
+ <% end %> +<% else %> +
Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?", turbo_confirm: "Are you sure?" }, method: :delete %>
+<% end %> <%= link_to "Back", :back %> diff --git a/lib/devise.rb b/lib/devise.rb index 3847e190c6..b29b8720e8 100644 --- a/lib/devise.rb +++ b/lib/devise.rb @@ -308,6 +308,10 @@ module Test mattr_accessor :sign_in_after_change_password @@sign_in_after_change_password = true + # When true, signed in users must provide their current password to cancel account + mattr_accessor :require_password_to_destroy + @@require_password_to_destroy = false + # Default way to set up Devise. Run rails generate devise_install to create # a fresh initializer with all configuration values. def self.setup diff --git a/lib/devise/models/registerable.rb b/lib/devise/models/registerable.rb index e55dac2723..9d7fdafdbc 100644 --- a/lib/devise/models/registerable.rb +++ b/lib/devise/models/registerable.rb @@ -23,6 +23,7 @@ def new_with_session(params, session) end Devise::Models.config(self, :sign_in_after_change_password) + Devise::Models.config(self, :require_password_to_destroy) end end end diff --git a/lib/devise/parameter_sanitizer.rb b/lib/devise/parameter_sanitizer.rb index 6d9523a4f5..2781516530 100644 --- a/lib/devise/parameter_sanitizer.rb +++ b/lib/devise/parameter_sanitizer.rb @@ -38,7 +38,8 @@ class ParameterSanitizer DEFAULT_PERMITTED_ATTRIBUTES = { sign_in: [:password, :remember_me], sign_up: [:password, :password_confirmation], - account_update: [:password, :password_confirmation, :current_password] + account_update: [:password, :password_confirmation, :current_password], + account_destroy: [:current_password] } def initialize(resource_class, resource_name, params) diff --git a/lib/generators/templates/devise.rb b/lib/generators/templates/devise.rb index 9e6744bd7d..11e070d3ac 100644 --- a/lib/generators/templates/devise.rb +++ b/lib/generators/templates/devise.rb @@ -162,6 +162,9 @@ # Defines which key will be used when confirming an account # config.confirmation_keys = [:email] + # If true, requires user to provide password to delete record. + config.require_password_to_destroy = false + # ==> Configuration for :rememberable # The time the user will be remembered without asking for credentials again. # config.remember_for = 2.weeks diff --git a/test/controllers/custom_registrations_controller_test.rb b/test/controllers/custom_registrations_controller_test.rb index 683322ebf8..6674e5c5aa 100644 --- a/test/controllers/custom_registrations_controller_test.rb +++ b/test/controllers/custom_registrations_controller_test.rb @@ -35,6 +35,20 @@ class CustomRegistrationsControllerTest < Devise::ControllerTestCase assert @controller.update_block_called?, "update failed to yield resource to provided block" end + test "yield resource to block on destroy failure" do + sign_in @user + delete :destroy, params: { user: { } } + assert @controller.destroy_block_called?, "destroy failed to yield resource to provided block" + end + + test "yield resource to block on destroy success" do + swap Devise, require_password_to_destroy: true do + sign_in @user + delete :destroy, params: { user: { current_password: @password } } + assert @controller.destroy_block_called?, "destroy failed to yield resource to provided block" + end + end + test "yield resource to block on new" do get :new assert @controller.new_block_called?, "new failed to yield resource to provided block" diff --git a/test/generators/views_generator_test.rb b/test/generators/views_generator_test.rb index 1f8f90f3ca..7592a4d612 100644 --- a/test/generators/views_generator_test.rb +++ b/test/generators/views_generator_test.rb @@ -109,8 +109,7 @@ def assert_shared_links(scope = nil) def assert_error_messages(scope = nil) scope = "devise" if scope.nil? - link = /<%= render \"#{scope}\/shared\/error_messages\", resource: resource %>/ - + link = /<%= render \"#{scope}\/shared\/error_messages\", resource: resource( if params\[:action\] == "(update|destroy)")? %>/ assert_file "app/views/#{scope}/passwords/edit.html.erb", link assert_file "app/views/#{scope}/passwords/new.html.erb", link assert_file "app/views/#{scope}/confirmations/new.html.erb", link diff --git a/test/integration/registerable_test.rb b/test/integration/registerable_test.rb index 038fcf7b91..cde157041d 100644 --- a/test/integration/registerable_test.rb +++ b/test/integration/registerable_test.rb @@ -271,6 +271,39 @@ def user_sign_up assert_empty User.to_adapter.find_all end + test 'a signed in user should be able to cancel their account with current password if password required' do + swap Devise, require_password_to_destroy: true do + sign_in_as_user + get edit_user_registration_path + + within(".destroy-password") do + fill_in 'current password', with: '12345678' + end + + click_button "Cancel my account" + assert_contain "Bye! Your account has been successfully cancelled. We hope to see you again soon." + end + assert User.to_adapter.find_all.empty? + end + + test 'a signed in user should not be able to cancel their account with incorrect password if password required' do + swap Devise, require_password_to_destroy: true do + sign_in_as_user + get edit_user_registration_path + + within(".destroy-password") do + fill_in 'current password', with: '87654321' + end + + click_button "Cancel my account" + assert_contain "Current password is invalid" + + assert_current_url '/users' + + refute User.to_adapter.find_all.empty? + end + end + test 'a user should be able to cancel sign up by deleting data in the session' do get "/set" assert_equal "something", @request.session["devise.foo_bar"] diff --git a/test/rails_app/app/controllers/custom/registrations_controller.rb b/test/rails_app/app/controllers/custom/registrations_controller.rb index dd0e7a2fa0..64af3033cc 100644 --- a/test/rails_app/app/controllers/custom/registrations_controller.rb +++ b/test/rails_app/app/controllers/custom/registrations_controller.rb @@ -19,6 +19,12 @@ def update end end + def destroy + super do |resource| + @destroy_block_called = true + end + end + def create_block_called? @create_block_called == true end @@ -30,4 +36,8 @@ def update_block_called? def new_block_called? @new_block_called == true end + + def destroy_block_called? + @destroy_block_called == true + end end