diff --git a/app/controllers/users/rules_of_use_controller.rb b/app/controllers/users/rules_of_use_controller.rb new file mode 100644 index 00000000000..76558f6f309 --- /dev/null +++ b/app/controllers/users/rules_of_use_controller.rb @@ -0,0 +1,51 @@ +module Users + class RulesOfUseController < ApplicationController + before_action :confirm_signed_in + before_action :confirm_need_to_accept_rules_of_use + + def new + analytics.track_event(Analytics::RULES_OF_USE_VISIT) + @rules_of_use_form = new_rules_of_use_form + render :new, formats: :html + end + + def create + @rules_of_use_form = new_rules_of_use_form + + result = @rules_of_use_form.submit(permitted_params) + + analytics.track_event(Analytics::RULES_OF_USE_SUBMITTED, result.to_h) + + if result.success? + process_successful_agreement_to_rules_of_use + else + render :new + end + end + + private + + def new_rules_of_use_form + RulesOfUseForm.new(current_user) + end + + def process_successful_agreement_to_rules_of_use + redirect_to user_two_factor_authentication_url + end + + def confirm_signed_in + return if signed_in? + redirect_to root_url + end + + def confirm_need_to_accept_rules_of_use + return unless current_user.accepted_terms_at + + redirect_to user_two_factor_authentication_url + end + + def permitted_params + params.require(:user).permit(:terms_accepted) + end + end +end diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb index d8415a5f826..6e02a97b590 100644 --- a/app/controllers/users/sessions_controller.rb +++ b/app/controllers/users/sessions_controller.rb @@ -109,7 +109,7 @@ def handle_valid_authentication create_user_event(:sign_in_before_2fa) update_sp_return_logs_with_user(current_user.id) update_last_sign_in_at_on_email - redirect_to_2fa_or_pending_reset + redirect_to next_url_after_valid_authentication end def now @@ -173,11 +173,13 @@ def request_id params.fetch(:request_id, '') end - def redirect_to_2fa_or_pending_reset + def next_url_after_valid_authentication if pending_account_reset_request.present? - redirect_to account_reset_pending_url + account_reset_pending_url + elsif current_user.accepted_rules_of_use? + user_two_factor_authentication_url else - redirect_to user_two_factor_authentication_url + rules_of_use_url end end diff --git a/app/forms/rules_of_use_form.rb b/app/forms/rules_of_use_form.rb new file mode 100644 index 00000000000..afef0cbf184 --- /dev/null +++ b/app/forms/rules_of_use_form.rb @@ -0,0 +1,42 @@ +class RulesOfUseForm + include ActiveModel::Model + include ActionView::Helpers::TranslationHelper + + validate :validate_terms_accepted + + attr_reader :terms_accepted + + def self.model_name + ActiveModel::Name.new(self, nil, 'User') + end + + def initialize(user) + @user = user + end + + def validate_terms_accepted + return if @terms_accepted + + errors.add(:terms_accepted, t('errors.rules_of_use')) + end + + def submit(params) + @terms_accepted = params[:terms_accepted] == 'true' + if valid? + process_successful_submission + else + self.success = false + end + + FormResponse.new(success: success, errors: errors) + end + + private + + attr_accessor :success, :user + + def process_successful_submission + self.success = true + UpdateUser.new(user: user, attributes: { accepted_terms_at: Time.zone.now }).call + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 49a3eec7f75..dfb61073161 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -64,6 +64,10 @@ def confirmed? email_addresses.where.not(confirmed_at: nil).any? end + def accepted_rules_of_use? + self.accepted_terms_at.present? + end + def set_reset_password_token super end diff --git a/app/services/analytics.rb b/app/services/analytics.rb index 3b6815fd47e..9bf7b3dce72 100644 --- a/app/services/analytics.rb +++ b/app/services/analytics.rb @@ -177,6 +177,8 @@ def browser_attributes REMEMBERED_DEVICE_USED_FOR_AUTH = 'Remembered device used for authentication'.freeze RETURN_TO_SP_CANCEL = 'Return to SP: Cancelled'.freeze RETURN_TO_SP_FAILURE_TO_PROOF = 'Return to SP: Failed to proof'.freeze + RULES_OF_USE_VISIT = 'Rules Of Use Visited'.freeze + RULES_OF_USE_SUBMITTED = 'Rules Of Use Submitted'.freeze SECURITY_EVENT_RECEIVED = 'RISC: Security event received'.freeze SP_REVOKE_CONSENT_REVOKED = 'SP Revoke Consent: Revoked'.freeze SP_REVOKE_CONSENT_VISITED = 'SP Revoke Consent: Visited'.freeze diff --git a/app/views/users/rules_of_use/new.html.erb b/app/views/users/rules_of_use/new.html.erb new file mode 100644 index 00000000000..dd80393041c --- /dev/null +++ b/app/views/users/rules_of_use/new.html.erb @@ -0,0 +1,36 @@ +<% title t('titles.registrations.new') %> + +

<%= t('titles.rules_of_use') %>

+ +

+<%= t('users.rules_of_use.overview_html', + link: new_window_link_to(t('titles.rules_of_use'), + MarketingSite.rules_of_use_url)) %> +

+ +<%= t('users.rules_of_use.details_html') %> +
+ <%= validated_form_for(@rules_of_use_form, + html: { autocomplete: 'off', role: 'form' }, + url: rules_of_use_path) do |f| %> + +
+ <%= f.check_box :terms_accepted, { class: 'usa-checkbox__input', + required: true, aria: { invalid: false } }, true, false %> + + +
+ + <%= f.button :button, t('forms.buttons.continue'), type: :submit, + class: 'usa-button--big grid-col-8 mobile-lg:grid-col-6' %> +<% end %> +
+ +<%= render 'shared/cancel', link: decorated_session.cancel_link_url %> + +<%= javascript_packs_tag_once 'accept-terms-button' %> diff --git a/config/locales/errors/en.yml b/config/locales/errors/en.yml index 9e4d96ea6b2..534ba7be711 100644 --- a/config/locales/errors/en.yml +++ b/config/locales/errors/en.yml @@ -103,6 +103,7 @@ en: registration: terms: Before you can continue, you must give us permission. Please check the box below and then click continue. + rules_of_use: Please check this box to continue two_factor_auth_setup: must_select_option: Select an authentication method. verify_personal_key: diff --git a/config/locales/errors/es.yml b/config/locales/errors/es.yml index b3bfa19ed09..48631fd4e34 100644 --- a/config/locales/errors/es.yml +++ b/config/locales/errors/es.yml @@ -107,6 +107,7 @@ es: registration: terms: Antes de continuar, debe darnos permiso. Marque la casilla a continuación y luego haga clic en continuar. + rules_of_use: Marque esta casilla para continuar two_factor_auth_setup: must_select_option: Seleccione un método de autenticación. verify_personal_key: diff --git a/config/locales/errors/fr.yml b/config/locales/errors/fr.yml index 83ea6e447e3..a7140d77fb2 100644 --- a/config/locales/errors/fr.yml +++ b/config/locales/errors/fr.yml @@ -116,6 +116,7 @@ fr: registration: terms: Avant de pouvoir continuer, vous devez nous donner la permission. Veuillez cocher la case ci-dessous puis cliquez sur continuer. + rules_of_use: Veuillez cocher cette case pour continuer two_factor_auth_setup: must_select_option: Sélectionnez une méthode d’authentification. verify_personal_key: diff --git a/config/locales/users/en.yml b/config/locales/users/en.yml index 262c194072d..813816b7396 100644 --- a/config/locales/users/en.yml +++ b/config/locales/users/en.yml @@ -24,3 +24,15 @@ en: generated_on_html: Generated on %{date} header: Your personal key print: Print + rules_of_use: + check_box_to_accept: Check this box to accept the login.gov + details_html: |- +
Rules of Use:
+ + overview_html: We’ve updated our %{link}. Please review and check the box below + to continue. diff --git a/config/locales/users/es.yml b/config/locales/users/es.yml index 88c4fe6263d..cf797834288 100644 --- a/config/locales/users/es.yml +++ b/config/locales/users/es.yml @@ -25,3 +25,15 @@ es: generated_on_html: Generado el %{date} header: Su clave personal print: Imprima esta página + rules_of_use: + check_box_to_accept: Marque esta casilla para aceptar las reglas de uso de login.gov + details_html: |- +
Reglas de uso:
+ + overview_html: Actualizamos nuestro %{link}. Revise y marque la casilla a + continuación para continuar. diff --git a/config/locales/users/fr.yml b/config/locales/users/fr.yml index 7bbe3aefb51..203fa9ebf53 100644 --- a/config/locales/users/fr.yml +++ b/config/locales/users/fr.yml @@ -27,3 +27,15 @@ fr: generated_on_html: Générée le %{date} header: Votre clé personnelle print: Imprimer cette page + rules_of_use: + check_box_to_accept: Cochez cette case pour accepter les règles d’utilisation de login.gov + details_html: |- +
Règles d’utilisation:
+ + overview_html: Nous avons mis à jour notre %{link}. Veuillez consulter et cocher + la case ci-dessous pour continuer. diff --git a/config/routes.rb b/config/routes.rb index 66e9d5861be..d4b8ea5de6c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -161,6 +161,9 @@ get '/events/disavow' => 'event_disavowal#new', as: :event_disavowal post '/events/disavow' => 'event_disavowal#create', as: :events_disavowal + get '/rules_of_use' => 'users/rules_of_use#new' + post '/rules_of_use' => 'users/rules_of_use#create' + get '/piv_cac' => 'users/piv_cac_authentication_setup#new', as: :setup_piv_cac get '/piv_cac_error' => 'users/piv_cac_authentication_setup#error', as: :setup_piv_cac_error delete '/piv_cac' => 'users/piv_cac_authentication_setup#delete', as: :disable_piv_cac diff --git a/spec/controllers/users/rules_of_use_controller_spec.rb b/spec/controllers/users/rules_of_use_controller_spec.rb new file mode 100644 index 00000000000..5a10fbb38fe --- /dev/null +++ b/spec/controllers/users/rules_of_use_controller_spec.rb @@ -0,0 +1,123 @@ +require 'rails_helper' + +RSpec.describe Users::RulesOfUseController do + describe 'before_actions' do + it 'includes appropriate before_actions' do + expect(subject).to have_actions( + :before, + :confirm_signed_in, + :confirm_need_to_accept_rules_of_use, + ) + end + end + + describe '#new' do + subject(:action) { get :new } + + context 'with a user that has not accepted the rules of use' do + before do + sign_in_before_2fa_with_user_that_needs_to_accept_rules_of_use + end + + it 'renders' do + action + expect(response).to render_template(:new) + end + + it 'logs an analytics event for visiting' do + stub_analytics + expect(@analytics).to receive(:track_event).with(Analytics::RULES_OF_USE_VISIT) + + action + end + end + + context 'with a user that has accepted the rules of use' do + before do + sign_in_before_2fa + end + + it 'redirects to mfa' do + action + + expect(response).to redirect_to user_two_factor_authentication_url + end + end + + context 'with no user signed in' do + it 'redirects to root' do + action + + expect(response).to redirect_to root_url + end + end + end + + describe '#create' do + context 'when the user needs to accept the rules of use and does accept them' do + subject(:action) do + post :create, params: { user: { terms_accepted: 'true' } } + end + + before do + sign_in_before_2fa_with_user_that_needs_to_accept_rules_of_use + end + + it 'updates the user accepted terms at timestamp' do + action + + expect(controller.current_user.reload.accepted_terms_at).to be_present + end + + it 'redirects to the two factor authentication page' do + action + + expect(response).to redirect_to user_two_factor_authentication_url + end + + it 'logs a successful analytics event' do + stub_analytics + expect(@analytics).to receive(:track_event). + with(Analytics::RULES_OF_USE_SUBMITTED, hash_including(success: true)) + + action + end + end + + context 'when the user needs to accept the rules of use and does not accept them' do + subject(:action) do + post :create, params: { user: { terms_accepted: 'false' } } + end + + before do + sign_in_before_2fa_with_user_that_needs_to_accept_rules_of_use + end + + it 'does not updates the user accepted terms at timestamp' do + action + + expect(controller.current_user.reload.accepted_terms_at).to be_nil + end + + it 'redirects to the two factor authentication page' do + action + + expect(response).to render_template(:new) + end + + it 'logs a failure analytics event' do + stub_analytics + expect(@analytics).to receive(:track_event). + with(Analytics::RULES_OF_USE_SUBMITTED, hash_including(success: false)) + + action + end + end + end + + def sign_in_before_2fa_with_user_that_needs_to_accept_rules_of_use + user = create(:user, :signed_up) + UpdateUser.new(user: user, attributes: {accepted_terms_at: nil}).call + sign_in_before_2fa(user) + end +end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 3cf048e54cd..8059bb9e53b 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -8,6 +8,7 @@ with { {} } email { Faker::Internet.safe_email } confirmed_at { Time.zone.now } + accepted_terms_at { Time.zone.now } end after(:build) do |user, evaluator| @@ -18,6 +19,7 @@ ) user.email = evaluator.email user.confirmed_at = evaluator.confirmed_at + user.accepted_terms_at = Time.zone.now end after(:stub) do |user, evaluator| @@ -28,6 +30,7 @@ ) user.email = evaluator.email user.confirmed_at = evaluator.confirmed_at + user.accepted_terms_at = Time.zone.now end trait :with_multiple_emails do diff --git a/spec/support/features/session_helper.rb b/spec/support/features/session_helper.rb index 77e4756a9a9..10692cbb671 100644 --- a/spec/support/features/session_helper.rb +++ b/spec/support/features/session_helper.rb @@ -17,6 +17,8 @@ def expect_email_invalid(page) end def choose_another_security_option(option) + accept_rules_of_use_and_continue_if_displayed + click_link t('two_factor_authentication.login_options_link_text') expect(current_path).to eq login_two_factor_options_path @@ -262,9 +264,16 @@ def sign_in_live_with_piv_cac(user = user_with_piv_cac) end def fill_in_code_with_last_phone_otp + accept_rules_of_use_and_continue_if_displayed fill_in :code, with: last_phone_otp end + def accept_rules_of_use_and_continue_if_displayed + return unless current_path == rules_of_use_path + check t('users.rules_of_use.check_box_to_accept'), allow_label_click: true + click_button t('forms.buttons.continue') + end + def click_submit_default click_button t('forms.buttons.submit.default') end diff --git a/spec/support/monitor/monitor_idp_steps.rb b/spec/support/monitor/monitor_idp_steps.rb index d9bbce230f6..70424655d63 100644 --- a/spec/support/monitor/monitor_idp_steps.rb +++ b/spec/support/monitor/monitor_idp_steps.rb @@ -57,6 +57,12 @@ def sign_in_and_2fa(email) fill_in 'user_email', with: email fill_in 'user_password', with: monitor.config.login_gov_sign_in_password click_on 'Sign in' + + if current_path == rules_of_use_path + check 'user_terms_accepted', allow_label_click: true + click_button 'Continue' + end + fill_in 'code', with: monitor.check_for_otp uncheck 'Remember this browser' click_on 'Submit'