]> git.openstreetmap.org Git - rails.git/commitdiff
Merge pull request #4313 from AntonKhorev/account-delete-delay
authorAndy Allan <git@gravitystorm.co.uk>
Wed, 22 Nov 2023 15:07:34 +0000 (15:07 +0000)
committerGitHub <noreply@github.com>
Wed, 22 Nov 2023 15:07:34 +0000 (15:07 +0000)
Account deletion cool-down period

14 files changed:
.rubocop_todo.yml
app/controllers/accounts_controller.rb
app/models/changeset.rb
app/models/user.rb
app/views/account/deletions/show.html.erb
config/initializers/config.rb
config/locales/en.yml
config/settings.yml
db/migrate/20231117170422_add_closed_at_index_to_changesets.rb [new file with mode: 0644]
db/structure.sql
test/controllers/accounts_controller_test.rb
test/models/user_test.rb
test/system/account_deletion_test.rb
test/test_helper.rb

index e3407c6dc0111d811f1b34377429a46d63a2d14a..bd2f92309886c83b83e4dfdaecd5cc8153f23cc7 100644 (file)
@@ -61,7 +61,7 @@ Metrics/BlockNesting:
 # Offense count: 26
 # Configuration parameters: CountComments, CountAsOne.
 Metrics/ClassLength:
-  Max: 286
+  Max: 297
 
 # Offense count: 59
 # Configuration parameters: AllowedMethods, AllowedPatterns.
index 63da1293ff731ecfe85d0430f9c6f0f1819a2008..db972101088b2a958fa0f3ffe25798fec41f1bbf 100644 (file)
@@ -53,12 +53,16 @@ class AccountsController < ApplicationController
   end
 
   def destroy
-    current_user.soft_destroy!
+    if current_user.deletion_allowed?
+      current_user.soft_destroy!
 
-    session.delete(:user)
-    session_expires_automatically
+      session.delete(:user)
+      session_expires_automatically
 
-    flash[:notice] = t ".success"
-    redirect_to root_path
+      flash[:notice] = t ".success"
+      redirect_to root_path
+    else
+      head :bad_request
+    end
   end
 end
index ce09438245844be0ff4ba241818973cdf26b5bab..137de18fd6479640c42688df2aceff3f56f1bfbb 100644 (file)
 #
 # Indexes
 #
-#  changesets_bbox_idx                (min_lat,max_lat,min_lon,max_lon) USING gist
-#  changesets_closed_at_idx           (closed_at)
-#  changesets_created_at_idx          (created_at)
-#  changesets_user_id_created_at_idx  (user_id,created_at)
-#  changesets_user_id_id_idx          (user_id,id)
+#  changesets_bbox_idx                        (min_lat,max_lat,min_lon,max_lon) USING gist
+#  changesets_closed_at_idx                   (closed_at)
+#  changesets_created_at_idx                  (created_at)
+#  changesets_user_id_created_at_idx          (user_id,created_at)
+#  changesets_user_id_id_idx                  (user_id,id)
+#  index_changesets_on_user_id_and_closed_at  (user_id,closed_at)
 #
 # Foreign Keys
 #
index 7571dd9dc5c8011f4615ebf970b6f0daa377adc9..1942a25cc237a784394b91f9ba64c844f36e3309 100644 (file)
@@ -419,6 +419,18 @@ class User < ApplicationRecord
     end
   end
 
+  def deletion_allowed_at
+    unless Settings.user_account_deletion_delay.nil?
+      last_changeset = changesets.reorder(:closed_at => :desc).first
+      return last_changeset.closed_at.utc + Settings.user_account_deletion_delay.hours if last_changeset
+    end
+    creation_time.utc
+  end
+
+  def deletion_allowed?
+    deletion_allowed_at <= Time.now.utc
+  end
+
   private
 
   def encrypt_password
index ddc8216772878300b3348b5ff55fd4f6608dbcc0..0ed4d663f03315944f9f7d5b1df1e82a81042f7e 100644 (file)
   <li><%= t ".retain_email" %></li>
 </ul>
 
-<%= link_to t(".delete_account"), account_path, { :method => :delete, :class => "btn btn-danger", :data => { :confirm => t(".confirm_delete") } } %>
+<% if current_user.deletion_allowed? %>
+  <%= link_to t(".delete_account"), account_path, { :method => :delete, :class => "btn btn-danger", :data => { :confirm => t(".confirm_delete") } } %>
+<% else %>
+  <div class="alert alert-warning">
+    <%= t ".recent_editing_html", :time => friendly_date(current_user.deletion_allowed_at) %>
+  </div>
+  <button class="btn btn-secondary" disabled><%= t(".delete_account") %></button>
+<% end %>
+
 <%= link_to t(".cancel"), edit_account_path, :class => "btn btn-link" %>
index c1cc522a5e13b24eef5b2bae373c512d403b82f6..f36e6ac1ac4f0686dc401f928c552fe1695478b4 100644 (file)
@@ -79,6 +79,7 @@ Config.setup do |config|
     required(:max_number_of_relation_members).filled(:int?)
     required(:max_issues_count).filled(:int?)
     required(:api_timeout).filled(:int?)
+    required(:user_account_deletion_delay).maybe(:number?)
     required(:imagery_blacklist).maybe(:array?)
     required(:status).filled(:str?, :included_in? => ALLOWED_STATUS)
     required(:avatar_storage).filled(:str?)
index 1a41dcce814de5f00882f7393c780f5b616f12c6..56d722f4331da17cd17d9145258f48c8f698a9f5 100644 (file)
@@ -256,6 +256,7 @@ en:
         retain_notes: Your map notes and note comments, if any, will be retained but hidden from view.
         retain_changeset_discussions: Your changeset discussions, if any, will be retained.
         retain_email: Your email address will be retained.
+        recent_editing_html: "As you have edited recently your account cannot currently be deleted. Deletion will be possible in %{time}."
         confirm_delete: Are you sure?
         cancel: Cancel
   accounts:
index 87c467c88301b11970ea43e365134f58f153bb0a..1c9c7e0a1123481543d2f2950b733fee71f5aeac 100644 (file)
@@ -53,6 +53,8 @@ api_timeout: 300
 web_timeout: 30
 # Periods (in hours) which are allowed for user blocks
 user_block_periods: [0, 1, 3, 6, 12, 24, 48, 96, 168, 336, 731, 4383, 8766, 87660]
+# Account deletion cooldown period (in hours) since last changeset close; null to disable, 0 to make sure there aren't any open changesets when the deletion happens
+user_account_deletion_delay: null
 # Rate limit for message sending
 max_messages_per_hour: 60
 # Rate limit for friending
diff --git a/db/migrate/20231117170422_add_closed_at_index_to_changesets.rb b/db/migrate/20231117170422_add_closed_at_index_to_changesets.rb
new file mode 100644 (file)
index 0000000..e9d7e62
--- /dev/null
@@ -0,0 +1,7 @@
+class AddClosedAtIndexToChangesets < ActiveRecord::Migration[7.1]
+  disable_ddl_transaction!
+
+  def change
+    add_index :changesets, [:user_id, :closed_at], :algorithm => :concurrently
+  end
+end
index 56e778523f7a7597d3a26670cc0dae97187a57e7..f74d4d571e2f58c8d3ef65009af3345d6128ea7b 100644 (file)
@@ -2499,6 +2499,13 @@ CREATE INDEX index_changeset_comments_on_changeset_id_and_created_at ON public.c
 CREATE INDEX index_changeset_comments_on_created_at ON public.changeset_comments USING btree (created_at);
 
 
+--
+-- Name: index_changesets_on_user_id_and_closed_at; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX index_changesets_on_user_id_and_closed_at ON public.changesets USING btree (user_id, closed_at);
+
+
 --
 -- Name: index_changesets_subscribers_on_changeset_id; Type: INDEX; Schema: public; Owner: -
 --
@@ -3499,6 +3506,7 @@ INSERT INTO "schema_migrations" (version) VALUES
 ('23'),
 ('22'),
 ('21'),
+('20231117170422'),
 ('20231101222146'),
 ('20231029151516'),
 ('20231010194809'),
index 7546c3797ee6f2939834dd1d9a0fc1b68587f6ba..131292f412d90f286b8798b81b84bde1015b2be4 100644 (file)
@@ -152,4 +152,23 @@ class AccountsControllerTest < ActionDispatch::IntegrationTest
     # Make sure we have a button to "go public"
     assert_select "form.button_to[action='/user/go_public']", true
   end
+
+  def test_destroy_allowed
+    user = create(:user)
+    session_for(user)
+
+    delete account_path
+    assert_response :redirect
+  end
+
+  def test_destroy_not_allowed
+    with_user_account_deletion_delay(24) do
+      user = create(:user)
+      create(:changeset, :user => user, :created_at => Time.now.utc)
+      session_for(user)
+
+      delete account_path
+      assert_response :bad_request
+    end
+  end
 end
index a4ed07e09eac0079a610f8e03762b0b89aeda105..5c48bb9698a6a73d56b7ab120b871d24902d18c5 100644 (file)
@@ -282,4 +282,62 @@ class UserTest < ActiveSupport::TestCase
     oauth_access_token.reload
     assert_predicate oauth_access_token, :revoked?
   end
+
+  def test_deletion_allowed_when_no_changesets
+    with_user_account_deletion_delay(10000) do
+      user = create(:user)
+      assert_predicate user, :deletion_allowed?
+    end
+  end
+
+  def test_deletion_allowed_without_delay
+    with_user_account_deletion_delay(nil) do
+      user = create(:user)
+      create(:changeset, :user => user)
+      user.reload
+      assert_predicate user, :deletion_allowed?
+    end
+  end
+
+  def test_deletion_allowed_past_delay
+    with_user_account_deletion_delay(10) do
+      user = create(:user)
+      create(:changeset, :user => user, :created_at => Time.now.utc - 12.hours, :closed_at => Time.now.utc - 10.hours)
+      user.reload
+      assert_predicate user, :deletion_allowed?
+    end
+  end
+
+  def test_deletion_allowed_during_delay
+    with_user_account_deletion_delay(10) do
+      user = create(:user)
+      create(:changeset, :user => user, :created_at => Time.now.utc - 11.hours, :closed_at => Time.now.utc - 9.hours)
+      user.reload
+      assert_not_predicate user, :deletion_allowed?
+      assert_equal Time.now.utc + 1.hour, user.deletion_allowed_at
+    end
+  end
+
+  def test_deletion_allowed_past_zero_delay
+    with_user_account_deletion_delay(0) do
+      user = create(:user)
+      create(:changeset, :user => user, :created_at => Time.now.utc, :closed_at => Time.now.utc + 1.hour)
+      travel 90.minutes do
+        user.reload
+        assert_predicate user, :deletion_allowed?
+      end
+    end
+  end
+
+  def test_deletion_allowed_during_zero_delay
+    with_user_account_deletion_delay(0) do
+      user = create(:user)
+      create(:changeset, :user => user, :created_at => Time.now.utc, :closed_at => Time.now.utc + 1.hour)
+      travel 30.minutes do
+        user.reload
+        assert_not_predicate user, :deletion_allowed?
+        assert_equal Time.now.utc + 30.minutes, user.deletion_allowed_at
+      end
+    end
+  end
 end
index 87e981c6426500443bcbf6811cef581f8ffce9b1..e6517dccc38b6bd0c5b17bb3463c5e34111bfdd3 100644 (file)
@@ -41,4 +41,59 @@ class AccountDeletionTest < ApplicationSystemTestCase
 
     assert_content "Account Deleted"
   end
+
+  test "can delete with any delay setting value if the user has no changesets" do
+    with_user_account_deletion_delay(10000) do
+      travel 1.hour do
+        visit edit_account_path
+
+        click_link "Delete Account..."
+
+        assert_no_content "cannot currently be deleted"
+      end
+    end
+  end
+
+  test "can delete with delay disabled" do
+    with_user_account_deletion_delay(nil) do
+      create(:changeset, :user => @user)
+
+      travel 1.hour do
+        visit edit_account_path
+
+        click_link "Delete Account..."
+
+        assert_no_content "cannot currently be deleted"
+      end
+    end
+  end
+
+  test "can delete when last changeset is old enough" do
+    with_user_account_deletion_delay(10) do
+      create(:changeset, :user => @user, :created_at => Time.now.utc, :closed_at => Time.now.utc + 1.hour)
+
+      travel 12.hours do
+        visit edit_account_path
+
+        click_link "Delete Account..."
+
+        assert_no_content "cannot currently be deleted"
+      end
+    end
+  end
+
+  test "can't delete when last changeset isn't old enough" do
+    with_user_account_deletion_delay(10) do
+      create(:changeset, :user => @user, :created_at => Time.now.utc, :closed_at => Time.now.utc + 1.hour)
+
+      travel 10.hours do
+        visit edit_account_path
+
+        click_link "Delete Account..."
+
+        assert_content "cannot currently be deleted"
+        assert_content "in about 1 hour"
+      end
+    end
+  end
 end
index 68749c0f70f595e00b322b21acee0215b7d46b51..19e1a2784611fa219af508a181909c7ead6f320c 100644 (file)
@@ -371,5 +371,16 @@ module ActiveSupport
         el << tag_el
       end
     end
+
+    def with_user_account_deletion_delay(value)
+      freeze_time
+      default_value = Settings.user_account_deletion_delay
+      Settings.user_account_deletion_delay = value
+
+      yield
+
+      Settings.user_account_deletion_delay = default_value
+      unfreeze_time
+    end
   end
 end