diff --git a/app/controllers/sign_up/cancellations_controller.rb b/app/controllers/sign_up/cancellations_controller.rb new file mode 100644 index 00000000000..e363212d3ff --- /dev/null +++ b/app/controllers/sign_up/cancellations_controller.rb @@ -0,0 +1,21 @@ +module SignUp + class CancellationsController < ApplicationController + before_action :ensure_in_setup + + def new + properties = ParseControllerFromReferer.new(request.referer).call + analytics.track_event(Analytics::USER_REGISTRATION_CANCELLATION, properties) + @presenter = CancellationPresenter.new(view_context: view_context) + end + + private + + def ensure_in_setup + redirect_to root_url if !session[:user_confirmation_token] && two_factor_enabled + end + + def two_factor_enabled + current_user && MfaPolicy.new(current_user).two_factor_enabled? + end + end +end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index dbc27e88ecb..3709c921c3e 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,4 +1,6 @@ class UsersController < ApplicationController + before_action :ensure_in_setup + def destroy track_account_deletion_event url_after_cancellation = decorated_session.cancel_link_url @@ -19,4 +21,12 @@ def destroy_user user&.destroy! sign_out if user end + + def ensure_in_setup + redirect_to root_url if !session[:user_confirmation_token] && two_factor_enabled + end + + def two_factor_enabled + current_user && MfaPolicy.new(current_user).two_factor_enabled? + end end diff --git a/app/presenters/cancellation_presenter.rb b/app/presenters/cancellation_presenter.rb new file mode 100644 index 00000000000..2194a4d9495 --- /dev/null +++ b/app/presenters/cancellation_presenter.rb @@ -0,0 +1,48 @@ +class CancellationPresenter < FailurePresenter + include ActionView::Helpers::TranslationHelper + include Rails.application.routes.url_helpers + + delegate :request, to: :view_context + + attr_reader :view_context + + def initialize(view_context:) + super(:warning) + @view_context = view_context + end + + def title + t('headings.cancellations.prompt') + end + + def header + t('headings.cancellations.prompt') + end + + def cancellation_warnings + [ + t('users.delete.bullet_1', app: APP_NAME), + t('users.delete.bullet_2_loa1'), + t('users.delete.bullet_3', app: APP_NAME), + ] + end + + def go_back_path + referer_path || two_factor_options_path + end + + private + + def referer_path + referer_string = request.env['HTTP_REFERER'] + return if referer_string.blank? + referer_uri = URI.parse(referer_string) + return if referer_uri.scheme == 'javascript' + return unless referer_uri.host == Figaro.env.domain_name.split(':')[0] + extract_path_and_query_from_uri(referer_uri) + end + + def extract_path_and_query_from_uri(uri) + [uri.path, uri.query].compact.join('?') + end +end diff --git a/app/services/analytics.rb b/app/services/analytics.rb index d096f4fbd65..3524443b719 100644 --- a/app/services/analytics.rb +++ b/app/services/analytics.rb @@ -129,6 +129,7 @@ def browser TWILIO_SMS_INBOUND_MESSAGE_VALIDATION_FAILED = 'Twilio SMS Inbound Validation Failed'.freeze USER_REGISTRATION_AGENCY_HANDOFF_PAGE_VISIT = 'User registration: agency handoff visited'.freeze USER_REGISTRATION_AGENCY_HANDOFF_COMPLETE = 'User registration: agency handoff complete'.freeze + USER_REGISTRATION_CANCELLATION = 'User registration: cancellation visited'.freeze USER_REGISTRATION_EMAIL = 'User Registration: Email Submitted'.freeze USER_REGISTRATION_EMAIL_CONFIRMATION = 'User Registration: Email Confirmation'.freeze USER_REGISTRATION_EMAIL_CONFIRMATION_RESEND = 'User Registration: Email Confirmation requested due to invalid token'.freeze diff --git a/app/views/shared/_cancel.html.slim b/app/views/shared/_cancel.html.slim index f5197c8b927..fb32d5866a7 100644 --- a/app/views/shared/_cancel.html.slim +++ b/app/views/shared/_cancel.html.slim @@ -1,12 +1,8 @@ -- cancel = sign_up_or_idv_no_js_link || link - other_cancel = link || decorated_session&.sp_return_url || profile_path .mt2.pt1.border-top - if user_signing_up? - - method = user_signing_up? ? :delete : :get - - = button_to cancel_link_text, cancel, method: method, - class: 'btn btn-link', id: 'auth-flow-cancel' + = button_to cancel_link_text, sign_up_cancel_path, method: :get, class: 'btn btn-link' = render 'shared/cancel_action_modal', idv: user_verifying_identity?, user_signing_up: user_signing_up? diff --git a/app/views/sign_up/cancellations/new.html.slim b/app/views/sign_up/cancellations/new.html.slim new file mode 100644 index 00000000000..e92791c1103 --- /dev/null +++ b/app/views/sign_up/cancellations/new.html.slim @@ -0,0 +1,12 @@ += render 'shared/failure', presenter: @presenter + +p.mb1.bold = t('sign_up.cancel.warning_header') + +ul class="list-reset #{@presenter.state_color}-dots" + - @presenter.cancellation_warnings.each do |warning| + li = warning + +.mt3 = button_to t('forms.buttons.cancel'), destroy_user_path, + method: :delete, class: 'btn btn-primary sm-col-6 col-12 btn-wide' +.mt2 = link_to t('links.go_back'), @presenter.go_back_path, + class: 'btn btn-secondary sm-col-6 col-12 btn-wide' diff --git a/config/locales/users/en.yml b/config/locales/users/en.yml index eb31ef6245b..3252223b891 100644 --- a/config/locales/users/en.yml +++ b/config/locales/users/en.yml @@ -5,9 +5,8 @@ en: actions: cancel: Cancel and return to your profile delete: Delete account - bullet_1: You won't have a %{app} account. - bullet_2_loa1: "%{app} will delete your email address, password, and phone number - from our system." + bullet_1: You won't have a %{app} account + bullet_2_loa1: We'll delete your email address, password, and phone number bullet_2_loa3: "%{app} will delete your email address, password, phone number, name, address, date of birth and Social Security number from our system." bullet_3: You won't be able to securely access your information using %{app}. diff --git a/config/locales/users/es.yml b/config/locales/users/es.yml index 0d618b1fb5e..bca4320f6b5 100644 --- a/config/locales/users/es.yml +++ b/config/locales/users/es.yml @@ -6,8 +6,8 @@ es: cancel: Anular y regresar a su perfil delete: Eliminar cuenta bullet_1: Usted no tendrá una %{app} cuenta. - bullet_2_loa1: "%{app} borrará su email, contraseña y número de teléfono de - nuestro sistema." + bullet_2_loa1: Eliminaremos su dirección de correo electrónico, contraseña y + número de teléfono bullet_2_loa3: "%{app} borrará su email, contraseña, número de teléfono, nombre, dirección, fecha de nacimiento y número de Seguro Social de nuestro sistema." bullet_3: Usted no podrá tener acceso seguro a su información usando %{app} diff --git a/config/locales/users/fr.yml b/config/locales/users/fr.yml index 9a56406bf80..ccae566f818 100644 --- a/config/locales/users/fr.yml +++ b/config/locales/users/fr.yml @@ -6,8 +6,8 @@ fr: cancel: Annuler et retourner à votre profil delete: Supprimer le compte bullet_1: Vous n'aurez pas de compte %{app}. - bullet_2_loa1: "%{app} supprimera votre adresse e-mail, votre mot de passe et - votre numéro de téléphone de notre système." + bullet_2_loa1: Nous effacerons votre adresse email, votre mot de passe et votre + numéro de téléphone bullet_2_loa3: "%{app} supprimera votre adresse e-mail, votre mot de passe et votre numéro de téléphone, votre nom, votre adresse, votre date de naissance et votre numéro de sécurité sociale de notre système." diff --git a/config/routes.rb b/config/routes.rb index d08c590d510..7392a243d92 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -173,6 +173,8 @@ get '/sign_up/verify_email' => 'sign_up/emails#show', as: :sign_up_verify_email get '/sign_up/completed' => 'sign_up/completions#show', as: :sign_up_completed post '/sign_up/completed' => 'sign_up/completions#update' + get '/sign_up/cancel/' => 'sign_up/cancellations#new', as: :sign_up_cancel + delete '/sign_up/cancel' => 'sign_up/cancellations#destroy' match '/sign_out' => 'sign_out#destroy', via: %i[get post delete] diff --git a/spec/controllers/sign_up/cancellations_controller_spec.rb b/spec/controllers/sign_up/cancellations_controller_spec.rb new file mode 100644 index 00000000000..f2001884301 --- /dev/null +++ b/spec/controllers/sign_up/cancellations_controller_spec.rb @@ -0,0 +1,30 @@ +require 'rails_helper' + +describe SignUp::CancellationsController do + describe '#new' do + it 'tracks the event in analytics when referer is nil' do + stub_sign_in + stub_analytics + properties = { request_came_from: 'no referer' } + + expect(@analytics).to receive(:track_event).with( + Analytics::USER_REGISTRATION_CANCELLATION, properties + ) + + get :new + end + + it 'tracks the event in analytics when referer is present' do + stub_sign_in + stub_analytics + request.env['HTTP_REFERER'] = 'http://example.com/' + properties = { request_came_from: 'users/sessions#new' } + + expect(@analytics).to receive(:track_event).with( + Analytics::USER_REGISTRATION_CANCELLATION, properties + ) + + get :new + end + end +end diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index 70a196645da..5a141b35deb 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -11,12 +11,25 @@ it 'destroys the current user and redirects to sign in page, with a helpful flash message' do sign_in_as_user + subject.session[:user_confirmation_token] = '1' expect { delete :destroy }.to change(User, :count).by(-1) expect(response).to redirect_to(root_url) expect(flash.now[:success]).to eq t('sign_up.cancel.success') end + it 'does not destroy the user if the user is not in setup mode and is after 2fa' do + sign_in_as_user + + expect { delete :destroy }.to change(User, :count).by(0) + end + + it 'does not destroy the user if the user is not in setup mode and is before 2fa' do + sign_in_before_2fa + + expect { delete :destroy }.to change(User, :count).by(0) + end + it 'finds the proper user and removes their record without `current_user`' do confirmation_token = '1' diff --git a/spec/features/saml/loa1/account_creation_spec.rb b/spec/features/saml/loa1/account_creation_spec.rb index e7c30477e6d..e4889c17f06 100644 --- a/spec/features/saml/loa1/account_creation_spec.rb +++ b/spec/features/saml/loa1/account_creation_spec.rb @@ -19,13 +19,30 @@ it 'redirects to the branded start page' do authn_request = auth_request.create(saml_settings) visit authn_request - sp_request_id = ServiceProviderRequest.last.uuid click_link t('sign_up.registrations.create_account') submit_form_with_valid_email click_confirmation_link_in_email('test@test.com') click_button t('links.cancel_account_creation') - expect(current_url).to eq sign_up_start_url(request_id: sp_request_id) + expect(current_url).to eq sign_up_cancel_url + + click_button t('forms.buttons.cancel') + expect(current_url).to eq sign_up_start_url(request_id: ServiceProviderRequest.last.uuid) + end + + it 'redirects to the password page after cancelling the cancellation' do + authn_request = auth_request.create(saml_settings) + visit authn_request + click_link t('sign_up.registrations.create_account') + submit_form_with_valid_email + click_confirmation_link_in_email('test@test.com') + previous_url = current_url + click_button t('links.cancel_account_creation') + + expect(current_url).to eq sign_up_cancel_url + + click_link t('links.go_back') + expect(current_url).to eq previous_url end end end diff --git a/spec/features/users/sign_up_spec.rb b/spec/features/users/sign_up_spec.rb index 5727048d600..b5c82affa95 100644 --- a/spec/features/users/sign_up_spec.rb +++ b/spec/features/users/sign_up_spec.rb @@ -26,7 +26,7 @@ end context 'user cancels on the enter password screen', email: true do - it 'returns them to the home page' do + it 'sends them to the cancel page' do email = 'test@test.com' visit sign_up_email_path @@ -36,7 +36,7 @@ click_on t('links.cancel_account_creation') - expect(current_path).to eq root_path + expect(current_path).to eq sign_up_cancel_path end end @@ -64,39 +64,6 @@ end context 'with js', js: true do - context 'sp loa1' do - it 'allows the user to toggle the modal' do - begin_sign_up_with_sp_and_loa(loa3: false) - expect(page).not_to have_xpath("//div[@id='cancel-action-modal']") - - click_on t('links.cancel') - expect(page).to have_xpath("//div[@id='cancel-action-modal']") - - click_on t('sign_up.buttons.continue') - expect(page).not_to have_xpath("//div[@id='cancel-action-modal']") - end - - it 'allows the user to delete their account and returns them to the branded start page' do - user = begin_sign_up_with_sp_and_loa(loa3: false) - - click_on t('links.cancel') - click_on t('sign_up.buttons.cancel') - - expect(page).to have_current_path(sign_up_start_path(request_id: '123')) - expect { User.find(user.id) }.to raise_error ActiveRecord::RecordNotFound - end - end - - context 'sp loa3' do - it 'behaves like loa1 when user has not finished sign up' do - begin_sign_up_with_sp_and_loa(loa3: true) - - click_on t('links.cancel') - - expect(page).to have_xpath("//input[@value=\"#{t('sign_up.buttons.cancel')}\"]") - end - end - context 'user enters their email as their password', email: true do it 'treats it as a weak password' do email = 'test@test.com' diff --git a/spec/presenters/cancellation_presenter_spec.rb b/spec/presenters/cancellation_presenter_spec.rb new file mode 100644 index 00000000000..a6dfa893276 --- /dev/null +++ b/spec/presenters/cancellation_presenter_spec.rb @@ -0,0 +1,62 @@ +require 'rails_helper' + +describe CancellationPresenter do + let(:good_url) { 'http://example.com/asdf/qwerty' } + let(:good_url_with_path) { 'http://example.com/asdf?qwerty=123' } + let(:bad_url) { 'http://evil.com/asdf/qwerty' } + + let(:view_context) { ActionView::Base.new } + + subject { described_class.new(view_context: view_context) } + + describe '#go_back_link' do + let(:sign_up_path) { '/two_factor_options' } + + before do + allow(view_context).to receive(:sign_up_path).and_return(sign_up_path) + request = instance_double(ActionDispatch::Request) + allow(request).to receive(:env).and_return('HTTP_REFERER' => referer_header) + allow(view_context).to receive(:request).and_return(request) + end + + context 'without a referer header' do + let(:referer_header) { nil } + + it 'returns the sign_up_path' do + expect(subject.go_back_path).to eq(sign_up_path) + end + end + + context 'with a referer header' do + let(:referer_header) { 'http://www.example.com/asdf/qwerty' } + + it 'returns the path' do + expect(subject.go_back_path).to eq('/asdf/qwerty') + end + end + + context 'with a referer header with query params' do + let(:referer_header) { 'http://www.example.com/asdf?qwerty=123' } + + it 'returns the path with the query params' do + expect(subject.go_back_path).to eq('/asdf?qwerty=123') + end + end + + context 'with a referer header for a different domain' do + let(:referer_header) { 'http://www.evil.com/asdf/qwerty' } + + it 'returns the sign_up_path' do + expect(subject.go_back_path).to eq(sign_up_path) + end + end + + context 'with a referer header with a javascript scheme' do + let(:referer_header) { 'javascript://do-some-evil-stuff' } + + it 'returns the sign_up_path' do + expect(subject.go_back_path).to eq(sign_up_path) + end + end + end +end