diff --git a/app/controllers/admin_user_controller.rb b/app/controllers/admin_user_controller.rb index 4c43fbd704..3a7d3d9882 100644 --- a/app/controllers/admin_user_controller.rb +++ b/app/controllers/admin_user_controller.rb @@ -131,6 +131,15 @@ def modify_comment_visibility redirect_back(fallback_location: admin_users_url) end + def account_closure_requests + @title = 'Account closure requests' + + # Find requests where the account associated with the request is not closed + @account_closure_requests = AccountClosureRequest. + joins(:user). + where(users: { closed_at: nil }) + end + private def user_params diff --git a/app/controllers/admin_users_account_closing_controller.rb b/app/controllers/admin_users_account_closing_controller.rb index 75e26fef7c..9ae2b5afb8 100644 --- a/app/controllers/admin_users_account_closing_controller.rb +++ b/app/controllers/admin_users_account_closing_controller.rb @@ -12,7 +12,7 @@ def create 'Something went wrong. The user account could not be closed.' end - redirect_to admin_user_path(@closed_user) + redirect_back_or_to admin_user_path(@closed_user) end private diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 260d379e1f..93270dcb78 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -29,6 +29,9 @@ class RouteNotFound < StandardError # Standard headers, footers and navigation for whole site layout "default" + # Defaults are :alert and :notice, but Alaveteli uses :error as well + add_flash_types :error + include FastGettext::Translation # make functions like _, n_, N_ etc available) include AlaveteliPro::PostRedirectHandler diff --git a/app/controllers/users/close_account_controller.rb b/app/controllers/users/close_account_controller.rb new file mode 100644 index 0000000000..6ae1e3ca3a --- /dev/null +++ b/app/controllers/users/close_account_controller.rb @@ -0,0 +1,46 @@ +# Handles the process of a user requesting to close their account. +class Users::CloseAccountController < ApplicationController + before_action :authenticate_user! + + def new + # Display a form that explains the process to the users + end + + def create + # If they haven't checked the "confirm" checkbox, redirect back to the form + if params[:confirm] == "0" + return redirect_to users_close_account_path, + error: "You must confirm that you want to close your account" + end + + # Otherwise, create a record of the user's request to close their account + current_user.create_account_closure_request! + + # Send the user an acknowledgement email + UserMailer.account_closure_requested(current_user).deliver_now + + # TODO: Should the user be logged out here? + + redirect_to root_path, + notice: "Your account closure request has been received. " \ + "We will be in touch." + end + + private + + def authenticate_user! + return if authenticated? + + ask_to_login( + web: _('To close your account on {{site_name}}', site_name: site_name), + email: _( + 'Then you can close your account on {{site_name}}', + site_name: site_name + ), + email_subject: _( + 'Close your account on {{site_name}}', + site_name: site_name + ) + ) + end +end diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index 56aeb3fef8..a1017e0e6d 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -45,4 +45,14 @@ def changeemail_already_used(old_email, new_email) to: new_email, subject: _("Unable to change email address on {{site_name}}", site_name: site_name)) end + + def account_closure_requested(user) + @name = user.name + + set_reply_to_headers(user) + mail_user( + user, + _("Your account closure request on {{site_name}}",site_name: site_name) + ) + end end diff --git a/app/models/account_closure_request.rb b/app/models/account_closure_request.rb new file mode 100644 index 0000000000..fda806d358 --- /dev/null +++ b/app/models/account_closure_request.rb @@ -0,0 +1,13 @@ +# == Schema Information +# Schema version: 20230718062820 +# +# Table name: account_closure_requests +# +# id :bigint not null, primary key +# user_id :bigint not null +# created_at :datetime not null +# updated_at :datetime not null +# +class AccountClosureRequest < ApplicationRecord + belongs_to :user +end diff --git a/app/models/outgoing_message.rb b/app/models/outgoing_message.rb index 80a489226f..f904706a5c 100644 --- a/app/models/outgoing_message.rb +++ b/app/models/outgoing_message.rb @@ -1,5 +1,5 @@ # == Schema Information -# Schema version: 20230412084830 +# Schema version: 20230718062820 # # Table name: outgoing_messages # diff --git a/app/models/user.rb b/app/models/user.rb index 526b893cb4..6e519a827e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -161,6 +161,10 @@ class User < ApplicationRecord inverse_of: :user, dependent: :destroy + has_one :account_closure_request, + inverse_of: :user, + dependent: :destroy + scope :active, -> { not_banned.not_closed } scope :banned, -> { where.not(ban_text: '') } scope :not_banned, -> { where(ban_text: '') } diff --git a/app/views/admin_general/_admin_navbar.html.erb b/app/views/admin_general/_admin_navbar.html.erb index 8838e892bd..f8e06c44b1 100644 --- a/app/views/admin_general/_admin_navbar.html.erb +++ b/app/views/admin_general/_admin_navbar.html.erb @@ -46,6 +46,7 @@
diff --git a/app/views/admin_user/_close_account_form.html.erb b/app/views/admin_user/_close_account_form.html.erb new file mode 100644 index 0000000000..ae3b4afd4c --- /dev/null +++ b/app/views/admin_user/_close_account_form.html.erb @@ -0,0 +1,9 @@ +<%= form_tag admin_users_account_closing_index_path(user_id: user.id), class: 'span3 form form-inline' do %> + <% disabled = user.closed? %> + <% submit_class = %w(btn btn-danger) %> + <% submit_class << 'disabled' if disabled %> + <%= submit_tag 'Close', + class: submit_class, + disabled: disabled, + data: { confirm: 'Are you sure? This is irreversible.' } %> +<% end %> diff --git a/app/views/admin_user/_scopes.html.erb b/app/views/admin_user/_scopes.html.erb index 1fe55bd316..2a2f68249d 100644 --- a/app/views/admin_user/_scopes.html.erb +++ b/app/views/admin_user/_scopes.html.erb @@ -22,6 +22,10 @@ <%= nav_li(admin_sign_ins_path) do %> <%= link_to 'Sign Ins', admin_sign_ins_path %> <% end %> + + <%= nav_li(account_closure_requests_admin_users_path) do %> + <%= link_to 'Account closure requests', account_closure_requests_admin_users_path %> + <% end %> diff --git a/app/views/admin_user/account_closure_requests.html.erb b/app/views/admin_user/account_closure_requests.html.erb new file mode 100644 index 0000000000..75c0c48758 --- /dev/null +++ b/app/views/admin_user/account_closure_requests.html.erb @@ -0,0 +1,35 @@ +<%= render 'scopes' %> + +Request ID | +User | +Created at | +Action | +
---|---|---|---|
<%= request.id %> | ++ <%= link_to request.user.name, admin_user_path(request.user) %> + | +<%= request.created_at.to_fs(:long) %> | ++ <%= render 'close_account_form', { user: request.user } %> + | +
No users have requested to close their accounts.
+ <% end %> +If you no longer wish to use your WhatDoTheyKnow account, you can ask us to close your account.
+ +Closing your account will:
+ +Closing your account will prevent you from logging in. If you have any requests that are ongoing, you will not be able to send any follow up messages to public authorities.
+ +<%= form_with url: users_close_account_path, method: :post do |form| %> ++ <%= form.check_box :confirm, class: "checkbox" %> + I understand that closing my account will mean that I will not be able to login or follow up on my requests, and that this cannot be undone. +
+ + <%= form.submit "Close my account", class: "button alert", data: { confirm: "Are you sure you want to close your account?" } %> +<% end %> diff --git a/config/routes.rb b/config/routes.rb index dae9301879..97e23da32f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -309,6 +309,9 @@ def matches?(request) get 'email_alerts/disable/:token', to: 'email_alerts#destroy', as: :disable_email_alerts + + get 'close_account', to: 'close_account#new', as: :close_account + post 'close_account', to: 'close_account#create' end namespace :users, path: 'profile' do @@ -710,6 +713,7 @@ def matches?(request) get 'active', :on => :collection get 'banned', :on => :collection get 'closed', :on => :collection + get 'account_closure_requests', :on => :collection get 'show_bounce_message', :on => :member post 'clear_bounce', :on => :member post 'clear_profile_photo', :on => :member diff --git a/db/migrate/20230718062820_create_account_closure_requests.rb b/db/migrate/20230718062820_create_account_closure_requests.rb new file mode 100644 index 0000000000..32dc89e10a --- /dev/null +++ b/db/migrate/20230718062820_create_account_closure_requests.rb @@ -0,0 +1,8 @@ +class CreateAccountClosureRequests < ActiveRecord::Migration[7.0] + def change + create_table :account_closure_requests do |t| + t.references :user, null: false, foreign_key: true + t.timestamps + end + end +end diff --git a/spec/controllers/admin_user_controller_spec.rb b/spec/controllers/admin_user_controller_spec.rb index ed2e47d478..a81368ce2f 100644 --- a/spec/controllers/admin_user_controller_spec.rb +++ b/spec/controllers/admin_user_controller_spec.rb @@ -449,4 +449,25 @@ }.to change(ActsAsXapian::ActsAsXapianJob, :count).by(1) end end + + describe "GET #account_closure_requests" do + it "assigns @account_closure_requests with open account" do + user_with_closed_account = FactoryBot.create(:user, closed_at: Time.now) + user_with_open_account = FactoryBot.create(:user) + request_for_closed_account = FactoryBot.create( + :account_closure_request, + user: user_with_closed_account + ) + request_for_open_account = FactoryBot.create( + :account_closure_request, + user: user_with_open_account + ) + + get :account_closure_requests + + expect( + assigns(:account_closure_requests) + ).to eq([request_for_open_account]) + end + end end diff --git a/spec/controllers/users/close_account_controller_spec.rb b/spec/controllers/users/close_account_controller_spec.rb new file mode 100644 index 0000000000..7e58f0db04 --- /dev/null +++ b/spec/controllers/users/close_account_controller_spec.rb @@ -0,0 +1,46 @@ +# spec/controllers/users/close_account_controller_spec.rb +require 'spec_helper' + +RSpec.describe Users::CloseAccountController, type: :controller do + describe "POST #create" do + let(:user) { FactoryBot.create(:user) } + + before do + sign_in user + end + + after do + user.account_closure_request&.destroy + end + + it "shows the user a confirmation page" do + get :new + assert_response :success + expect(response).to render_template(:new) + end + + it "asks the user to check the confirmation checkbox" do + post :create, params: { confirm: "0" } + assert_response :redirect + expect(response).to redirect_to(users_close_account_path) + expect(flash[:error]).to eq("You must confirm that you want to close your account") + end + + it "creates a record of the user's request to close their account" do + post :create, params: { confirm: "1" } + + user.reload + expect(user.account_closure_request).to be_present + + # Check email has been sent + expect(ActionMailer::Base.deliveries.count).to eq(1) + email = ActionMailer::Base.deliveries.last + expect(email.to).to eq([user.email]) + expect(email.subject).to eq("Your account closure request on #{site_name}") + + assert_response :redirect + expect(response).to redirect_to(root_path) + expect(flash[:notice]).to eq("Your account closure request has been received. We will be in touch.") + end + end +end diff --git a/spec/factories/account_closure_requests.rb b/spec/factories/account_closure_requests.rb new file mode 100644 index 0000000000..c2dbc6da5e --- /dev/null +++ b/spec/factories/account_closure_requests.rb @@ -0,0 +1,14 @@ +# == Schema Information +# Schema version: 20230718062820 +# +# Table name: account_closure_requests +# +# id :bigint not null, primary key +# user_id :bigint not null +# created_at :datetime not null +# updated_at :datetime not null +# +FactoryBot.define do + factory :account_closure_request do + end +end diff --git a/spec/integration/admin_user_account_closing_spec.rb b/spec/integration/admin_user_account_closing_spec.rb new file mode 100644 index 0000000000..74441a2123 --- /dev/null +++ b/spec/integration/admin_user_account_closing_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' +require 'integration/alaveteli_dsl' + +RSpec.describe 'Admin Account Closure Requests' do + before do + allow(AlaveteliConfiguration).to receive(:skip_admin_auth).and_return(false) + + confirm(:admin_user) + @admin = login(:admin_user) + @user = FactoryBot.create(:user) + @account_closure_request = FactoryBot.create( + :account_closure_request, + user: @user + ) + end + + context 'viewing account closure requests' do + it 'displays link to "Account closure requests" on admin homepage' do + using_session(@admin) do + visit admin_general_index_path + expect(page).to have_link('Account closure requests') + end + end + + it 'can close an account from the "Account closure requests" page' do + using_session(@admin) do + expect(@user).to_not be_closed + + visit account_closure_requests_admin_users_path + within("tr#account-closure-request-#{@account_closure_request.id}") do + click_button 'Close' + end + expect(page).to have_text('The user account was closed.') + + @user.reload + expect(@user).to be_closed + + visit account_closure_requests_admin_users_path + expect(page).to_not have_text(@user.name) + end + end + end +end