diff --git a/Gemfile b/Gemfile index a6ea2e90942..a645422ddb6 100644 --- a/Gemfile +++ b/Gemfile @@ -102,7 +102,7 @@ group :development, :test do gem 'brakeman', require: false gem 'bullet', '~> 7.0' gem 'capybara-webmock', git: 'https://github.com/hashrocket/capybara-webmock.git', ref: 'd3f3b7c' - gem 'erb_lint', '~> 0.4.0', require: false + gem 'erb_lint', '~> 0.5.0', require: false gem 'i18n-tasks', '~> 1.0' gem 'knapsack' gem 'listen' diff --git a/Gemfile.lock b/Gemfile.lock index 0a7fa861485..26a02875269 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -153,8 +153,8 @@ GEM minitest (>= 5.1) mutex_m tzinfo (~> 2.0) - addressable (2.8.6) - public_suffix (>= 2.0.2, < 6.0) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) ahoy_matey (3.3.0) activesupport (>= 5) device_detector @@ -197,11 +197,11 @@ GEM aws-sigv4 (~> 1.1) aws-sigv4 (1.8.0) aws-eventstream (~> 1, >= 1.0.2) - axe-core-api (4.9.0) + axe-core-api (4.9.1) dumb_delegator virtus - axe-core-rspec (4.9.0) - axe-core-api + axe-core-rspec (4.9.1) + axe-core-api (= 4.9.1) dumb_delegator virtus axiom-types (0.1.1) @@ -217,7 +217,7 @@ GEM erubi (>= 1.0.0) rack (>= 0.9.0) rouge (>= 1.0.0) - better_html (2.0.2) + better_html (2.1.1) actionview (>= 6.0) activesupport (>= 6.0) ast (~> 2.0) @@ -230,7 +230,7 @@ GEM msgpack (~> 1.2) brakeman (6.1.0) browser (6.0.0) - builder (3.2.4) + builder (3.3.0) bullet (7.1.4) activesupport (>= 3.0.0) uniform_notifier (~> 1.11) @@ -257,12 +257,13 @@ GEM coderay (1.1.3) coercible (1.0.0) descendants_tracker (~> 0.0.1) - concurrent-ruby (1.3.1) + concurrent-ruby (1.3.3) connection_pool (2.4.1) cose (1.3.0) cbor (~> 0.5.9) openssl-signature_algorithm (~> 1.0) - crack (0.4.5) + crack (1.0.0) + bigdecimal rexml crass (1.0.6) css_parser (1.14.0) @@ -304,7 +305,7 @@ GEM htmlentities (~> 4.3.3) launchy (~> 2.1) mail (~> 2.7) - erb_lint (0.4.0) + erb_lint (0.5.0) activesupport better_html (>= 2.0.1) parser (>= 2.7.1.4) @@ -312,7 +313,7 @@ GEM rubocop smart_properties errbase (0.2.1) - erubi (1.12.0) + erubi (1.13.0) et-orbi (1.2.7) tzinfo factory_bot (6.4.6) @@ -349,11 +350,11 @@ GEM railties (>= 6.0.0) thor (>= 0.14.1) google-protobuf (3.24.4) - hashdiff (1.0.1) + hashdiff (1.1.0) heapy (0.2.0) thor highline (2.1.0) - htmlbeautifier (1.4.2) + htmlbeautifier (1.4.3) htmlentities (4.3.4) http_accept_language (2.1.1) i18n (1.14.5) @@ -371,13 +372,13 @@ GEM terminal-table (>= 1.5.1) ice_nine (0.11.2) io-console (0.7.2) - irb (1.13.1) + irb (1.13.2) rdoc (>= 4.0.0) reline (>= 0.4.2) jmespath (1.6.2) jsbundling-rails (1.1.2) railties (>= 6.0.0) - json (2.7.1) + json (2.7.2) jwe (0.4.0) jwt (2.7.1) knapsack (4.0.0) @@ -390,6 +391,7 @@ GEM listen (3.8.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) + logger (1.6.0) lograge (0.11.2) actionpack (>= 4) activesupport (>= 4) @@ -424,7 +426,7 @@ GEM mini_histogram (0.3.1) mini_mime (1.1.5) mini_portile2 (2.8.7) - minitest (5.23.1) + minitest (5.24.1) msgpack (1.7.2) multiset (0.5.3) mutex_m (0.2.0) @@ -444,18 +446,18 @@ GEM net-ssh (6.1.0) newrelic_rpm (9.7.0) nio4r (2.7.3) - nokogiri (1.16.5) + nokogiri (1.16.6) mini_portile2 (~> 2.8.2) racc (~> 1.4) openssl (3.0.2) openssl-signature_algorithm (1.2.1) openssl (> 2.0, < 3.1) orm_adapter (0.5.0) - parallel (1.24.0) - parser (3.3.1.0) + parallel (1.25.1) + parser (3.3.3.0) ast (~> 2.4.1) racc - pg (1.5.4) + pg (1.5.6) pg_query (4.2.3) google-protobuf (>= 3.22.3) phonelib (0.8.9) @@ -489,7 +491,7 @@ GEM pry (>= 0.10.4) psych (5.1.2) stringio - public_suffix (5.0.5) + public_suffix (6.0.0) puma (6.4.2) nio4r (~> 2.0) raabro (1.4.0) @@ -558,12 +560,12 @@ GEM psych (>= 4.0.0) redacted_struct (2.0.0) redcarpet (3.6.0) - redis (5.1.0) - redis-client (>= 0.17.0) - redis-client (0.22.0) + redis (5.2.0) + redis-client (>= 0.22.0) + redis-client (0.22.2) connection_pool - regexp_parser (2.9.1) - reline (0.5.8) + regexp_parser (2.9.2) + reline (0.5.9) io-console (~> 0.5) request_store (1.5.1) rack (>= 1.4) @@ -571,8 +573,8 @@ GEM actionpack (>= 5.0) railties (>= 5.0) retries (0.0.5) - rexml (3.2.8) - strscan (>= 3.0.9) + rexml (3.3.1) + strscan rotp (6.3.0) rouge (4.2.0) rqrcode (2.1.0) @@ -615,8 +617,8 @@ GEM rubocop-ast (>= 1.31.1, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.31.2) - parser (>= 3.3.0.4) + rubocop-ast (1.31.3) + parser (>= 3.3.1.0) rubocop-capybara (2.19.0) rubocop (~> 1.41) rubocop-factory_bot (2.24.0) @@ -647,8 +649,9 @@ GEM jwt (~> 2.0) scrypt (3.0.7) ffi-compiler (>= 1.0, < 2.0) - selenium-webdriver (4.20.1) + selenium-webdriver (4.22.0) base64 (~> 0.2) + logger (~> 1.4) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) @@ -673,9 +676,9 @@ GEM unf (~> 0.1.4) smart_properties (1.17.0) stringex (2.8.5) - stringio (3.1.0) - strong_migrations (1.6.4) - activerecord (>= 5.2) + stringio (3.1.1) + strong_migrations (2.0.0) + activerecord (>= 6.1) strscan (3.1.0) tableparser (1.0.1) terminal-table (3.0.2) @@ -734,7 +737,7 @@ GEM xpath (3.2.0) nokogiri (~> 1.8) yard (0.9.36) - zeitwerk (2.6.15) + zeitwerk (2.6.16) zlib (3.0.0) zonebie (0.6.1) zxcvbn (0.1.9) @@ -773,7 +776,7 @@ DEPENDENCIES devise (~> 4.8) dotiw (>= 4.0.1) email_spec - erb_lint (~> 0.4.0) + erb_lint (~> 0.5.0) factory_bot_rails (>= 6.2.0) faker faraday (~> 2) diff --git a/app/assets/stylesheets/email.css.scss b/app/assets/stylesheets/email.css.scss index fd485da9ec4..d3310b4f25d 100644 --- a/app/assets/stylesheets/email.css.scss +++ b/app/assets/stylesheets/email.css.scss @@ -119,14 +119,6 @@ h6 { } } -@media only screen and (max-width: #{$global-breakpoint}) { - table.body .columns, - table.body .column { - padding-left: $global-gutter-small !important; - padding-right: $global-gutter-small !important; - } -} - .info-alert, .warning-alert { padding: 0 units(0.5); diff --git a/app/components/badge_component.scss b/app/components/badge_component.scss index 2dbbf933fc0..4958757cead 100644 --- a/app/components/badge_component.scss +++ b/app/components/badge_component.scss @@ -1,7 +1,2 @@ @use 'uswds-core' as *; @forward 'usa-verification-badge'; - -// Upstream: https://github.com/18F/identity-design-system/pull/445 -.lg-verification-badge .usa-icon { - margin-right: units(1); -} diff --git a/app/components/one_time_code_input_component.scss b/app/components/one_time_code_input_component.scss index 6bbf026d0e2..1d43f96a5a0 100644 --- a/app/components/one_time_code_input_component.scss +++ b/app/components/one_time_code_input_component.scss @@ -2,12 +2,13 @@ lg-one-time-code-input { display: block; - - @include at-media('tablet') { - width: 50%; - } } .one-time-code-input__input { + &.usa-input { + @include at-media('tablet') { + max-width: 50%; + } + } @include u-font-family('mono'); } diff --git a/app/controllers/concerns/idv_step_concern.rb b/app/controllers/concerns/idv_step_concern.rb index fccc07a6f34..2fd730152ad 100644 --- a/app/controllers/concerns/idv_step_concern.rb +++ b/app/controllers/concerns/idv_step_concern.rb @@ -3,6 +3,7 @@ module IdvStepConcern extend ActiveSupport::Concern + include VerifyProfileConcern include IdvSessionConcern include RateLimitConcern include FraudReviewConcern @@ -14,9 +15,7 @@ module IdvStepConcern before_action :confirm_personal_key_acknowledged_if_needed before_action :confirm_idv_needed before_action :confirm_letter_recently_enqueued - before_action :confirm_no_pending_gpo_profile - before_action :confirm_no_pending_in_person_enrollment - before_action :handle_fraud + before_action :confirm_no_pending_profile before_action :check_for_mail_only_outage end @@ -35,13 +34,8 @@ def confirm_letter_recently_enqueued return redirect_to idv_letter_enqueued_url if letter_recently_enqueued? end - def confirm_no_pending_gpo_profile - redirect_to idv_verify_by_mail_enter_code_url if letter_not_recently_enqueued? - end - - def confirm_no_pending_in_person_enrollment - return if !IdentityConfig.store.in_person_proofing_enabled - redirect_to idv_in_person_ready_to_verify_url if current_user&.pending_in_person_enrollment + def confirm_no_pending_profile + redirect_to url_for_pending_profile_reason if user_has_pending_profile? end def check_for_mail_only_outage diff --git a/app/controllers/concerns/verify_profile_concern.rb b/app/controllers/concerns/verify_profile_concern.rb index 7d9a779a0fe..e65f16f3832 100644 --- a/app/controllers/concerns/verify_profile_concern.rb +++ b/app/controllers/concerns/verify_profile_concern.rb @@ -6,15 +6,12 @@ module VerifyProfileConcern def url_for_pending_profile_reason return idv_verify_by_mail_enter_code_url if current_user.gpo_verification_pending_profile? return idv_in_person_ready_to_verify_url if current_user.in_person_pending_profile? - # We don't want to hit idv_please_call_url in cases where the user - # has fraud review pending and not passed at the post office - return idv_welcome_url if user_failed_ipp_with_fraud_review_pending? return idv_please_call_url if current_user.fraud_review_pending? idv_not_verified_url if current_user.fraud_rejection? end def user_has_pending_profile? - pending_profile_policy.user_has_pending_profile? + pending_profile_policy.user_has_pending_profile? && !user_failed_ipp_with_fraud_review_pending? end def pending_profile_policy @@ -25,6 +22,10 @@ def pending_profile_policy ) end + # Returns true if the user has not passed IPP at the post office and is + # flagged for fraud review, or has been rejected for fraud. + # Ultimately this is to allow users who fail at the post office to create another enrollment + # bypassing the typical flow of showing the Please Call or Fraud Rejection screens. def user_failed_ipp_with_fraud_review_pending? IdentityConfig.store.in_person_proofing_enforce_tmx && current_user.ipp_enrollment_status_not_passed? && diff --git a/app/controllers/idv/in_person/public/usps_locations_controller.rb b/app/controllers/idv/in_person/public/usps_locations_controller.rb index eafddf1924a..5ecb9c28ae1 100644 --- a/app/controllers/idv/in_person/public/usps_locations_controller.rb +++ b/app/controllers/idv/in_person/public/usps_locations_controller.rb @@ -18,26 +18,19 @@ def index ) locations = proofer.request_facilities(candidate, false) - render json: localized_locations(locations).to_json + render json: locations.to_json end def options head :ok end - private + protected def proofer @proofer ||= UspsInPersonProofing::EnrollmentHelper.usps_proofer end - def localized_locations(locations) - return nil if locations.nil? - locations.map do |location| - UspsInPersonProofing::EnrollmentHelper.localized_location(location) - end - end - def enabled? IdentityConfig.store.in_person_public_address_search_enabled end diff --git a/app/controllers/idv/in_person/usps_locations_controller.rb b/app/controllers/idv/in_person/usps_locations_controller.rb index 45cac7ac484..4367b795ba3 100644 --- a/app/controllers/idv/in_person/usps_locations_controller.rb +++ b/app/controllers/idv/in_person/usps_locations_controller.rb @@ -27,18 +27,18 @@ def index zip_code: search_params['zip_code'] ) is_enhanced_ipp = resolved_authn_context_result.enhanced_ipp? - locations = proofer.request_facilities(candidate, is_enhanced_ipp) - if locations.length > 0 + response = proofer.request_facilities(candidate, is_enhanced_ipp) + if response.length > 0 analytics.idv_in_person_locations_searched( success: true, - result_total: locations.length, + result_total: response.length, ) else analytics.idv_in_person_locations_searched( success: false, errors: 'No USPS locations found', ) end - render json: localized_locations(locations).to_json + render json: response.to_json end # save the Post Office location the user selected to an enrollment @@ -64,13 +64,6 @@ def add_proofing_component update(document_check: Idp::Constants::Vendors::USPS) end - def localized_locations(locations) - return nil if locations.nil? - locations.map do |location| - EnrollmentHelper.localized_location(location) - end - end - def handle_error(err) remapped_error = case err when ActionController::InvalidAuthenticityToken, diff --git a/app/controllers/idv/personal_key_controller.rb b/app/controllers/idv/personal_key_controller.rb index 4d18dcbc6b1..7cccf2d8924 100644 --- a/app/controllers/idv/personal_key_controller.rb +++ b/app/controllers/idv/personal_key_controller.rb @@ -16,8 +16,7 @@ class PersonalKeyController < ApplicationController # standard before_actions and handle them in our own special way below. skip_before_action :confirm_idv_needed skip_before_action :confirm_personal_key_acknowledged_if_needed - skip_before_action :confirm_no_pending_in_person_enrollment - skip_before_action :handle_fraud + skip_before_action :confirm_no_pending_profile def show analytics.idv_personal_key_visited( diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb index 135ef08f9cd..159e784baf2 100644 --- a/app/controllers/users/sessions_controller.rb +++ b/app/controllers/users/sessions_controller.rb @@ -74,10 +74,21 @@ def increment_session_bad_password_count def process_locked_out_session warden.logout(:user) warden.lock! - flash[:error] = t('errors.sign_in.bad_password_limit') + + flash[:error] = t( + 'errors.sign_in.bad_password_limit', + time_left: locked_out_time_remaining, + ) redirect_to root_url end + def locked_out_time_remaining + locked_at = session[:max_bad_passwords_at] + window = IdentityConfig.store.max_bad_passwords_window_in_seconds.seconds + time_lockout_expires = Time.zone.at(locked_at) + window + distance_of_time_in_words(Time.zone.now, time_lockout_expires, true) + end + def valid_captcha_result? return @valid_captcha_result if defined?(@valid_captcha_result) @valid_captcha_result = SignInRecaptchaForm.new(**recaptcha_form_args).submit( diff --git a/app/javascript/packages/clipboard-button/package.json b/app/javascript/packages/clipboard-button/package.json index 86ae7f2f7b1..bd1df2ce1e0 100644 --- a/app/javascript/packages/clipboard-button/package.json +++ b/app/javascript/packages/clipboard-button/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "private": true, "dependencies": { - "@18f/identity-design-system": "^9.2.0" + "@18f/identity-design-system": "^9.3.0" }, "sideEffects": [ "./clipboard-button-element.ts" diff --git a/app/javascript/packages/components/alert.spec.tsx b/app/javascript/packages/components/alert.spec.tsx index 917247d6f05..47a645048ef 100644 --- a/app/javascript/packages/components/alert.spec.tsx +++ b/app/javascript/packages/components/alert.spec.tsx @@ -5,15 +5,14 @@ import type { AlertType } from './alert'; describe('Alert', () => { describe('role', () => { - ( - [ - ['success', 'status'], - ['warning', 'status'], - ['error', 'alert'], - ['info', 'status'], - ['other', 'status'], - ] as [AlertType, 'alert' | 'status'][] - ).forEach(([type, role]) => { + const variants: [AlertType, 'alert' | 'status'][] = [ + ['success', 'status'], + ['warning', 'status'], + ['error', 'alert'], + ['info', 'status'], + ]; + + variants.forEach(([type, role]) => { context(`with ${type} type`, () => { it(`should apply ${role} role`, () => { const { getByRole } = render(); diff --git a/app/javascript/packages/components/alert.tsx b/app/javascript/packages/components/alert.tsx index 033b5bafdbe..ca2f3e65620 100644 --- a/app/javascript/packages/components/alert.tsx +++ b/app/javascript/packages/components/alert.tsx @@ -1,11 +1,11 @@ import { forwardRef, createElement } from 'react'; import type { ReactNode, ForwardedRef } from 'react'; -export type AlertType = 'success' | 'warning' | 'error' | 'info' | 'other'; +export type AlertType = 'success' | 'warning' | 'error' | 'info'; interface AlertProps { /** - * Alert type. Defaults to "other". + * Alert type. */ type?: AlertType; @@ -32,10 +32,10 @@ interface AlertProps { } function Alert( - { type = 'other', className, isFocusable, children, textTag = 'p' }: AlertProps, + { type, className, isFocusable, children, textTag = 'p' }: AlertProps, ref: ForwardedRef, ) { - const classes = [`usa-alert usa-alert--${type}`, className].filter(Boolean).join(' '); + const classes = ['usa-alert', type && `usa-alert--${type}`, className].filter(Boolean).join(' '); const role = type === 'error' ? 'alert' : 'status'; const inner = createElement(textTag, { className: 'usa-alert__text' }, children); diff --git a/app/javascript/packages/stylelint-config/CHANGELOG.md b/app/javascript/packages/stylelint-config/CHANGELOG.md index 93fb812367e..cbc85a1753c 100644 --- a/app/javascript/packages/stylelint-config/CHANGELOG.md +++ b/app/javascript/packages/stylelint-config/CHANGELOG.md @@ -1,4 +1,4 @@ -## 5.0.0-beta.1 +## Unreleased ### Breaking Changes @@ -13,6 +13,8 @@ - [`scss/double-slash-comment-empty-line-before`](https://github.com/stylelint-scss/stylelint-scss/blob/master/src/rules/double-slash-comment-empty-line-before/README.md) - [`color-function-notation`](https://stylelint.io/user-guide/rules/color-function-notation/) (due to [Sass incompatibilities](https://github.com/sass/sass/issues/2831)) - The ruleset now configures [`"reportNeedlessDisables": true`](https://stylelint.io/user-guide/options/#reportneedlessdisables), which will report inline configuration that disables rules unnecessarily. +- The [`declaration-no-important`](https://stylelint.io/user-guide/rules/declaration-no-important/) rule is now enabled, which disallows `!important` in stylesheets. + - `!important` is a sledgehammer solution which often causes more problems than it helps, and usually stems from misunderstandings of [CSS specificity](https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity). See related ["Best practices" MDN documentation](https://developer.mozilla.org/en-US/docs/Web/CSS/important#best_practices). ## 4.1.0 diff --git a/app/javascript/packages/stylelint-config/index.js b/app/javascript/packages/stylelint-config/index.js index 16f79869db9..01e5818316d 100644 --- a/app/javascript/packages/stylelint-config/index.js +++ b/app/javascript/packages/stylelint-config/index.js @@ -4,6 +4,7 @@ module.exports = { 'at-rule-empty-line-before': null, 'color-function-notation': null, 'declaration-empty-line-before': null, + 'declaration-no-important': true, 'no-descending-specificity': null, 'rule-empty-line-before': null, 'scss/comment-no-empty': null, diff --git a/app/presenters/idv/in_person/ready_to_verify_presenter.rb b/app/presenters/idv/in_person/ready_to_verify_presenter.rb index 61caeca8b5a..a5f8462f742 100644 --- a/app/presenters/idv/in_person/ready_to_verify_presenter.rb +++ b/app/presenters/idv/in_person/ready_to_verify_presenter.rb @@ -31,7 +31,7 @@ def formatted_due_date def selected_location_hours(prefix) return unless selected_location_details hours = selected_location_details["#{prefix}_hours"] - UspsInPersonProofing::EnrollmentHelper.localized_hours(hours) if hours + return localized_hours(hours) if hours end def service_provider @@ -107,6 +107,18 @@ def format_outage_date(date) I18n.l(date.to_date, format: :short) end + def localized_hours(hours) + case hours + when 'Closed' + I18n.t('in_person_proofing.body.barcode.retail_hours_closed') + else + hours. + split(' - '). # Hyphen + map { |time| Time.zone.parse(time).strftime(I18n.t('time.formats.event_time')) }. + join(' – ') # Endash + end + end + def sp_return_url_resolver SpReturnUrlResolver.new(service_provider: service_provider) end diff --git a/app/services/usps_in_person_proofing/enrollment_helper.rb b/app/services/usps_in_person_proofing/enrollment_helper.rb index cb8cc789c9f..7de2212dc26 100644 --- a/app/services/usps_in_person_proofing/enrollment_helper.rb +++ b/app/services/usps_in_person_proofing/enrollment_helper.rb @@ -101,40 +101,6 @@ def usps_proofer end end - def localized_location(location) - { - address: location.address, - city: location.city, - distance: location.distance, - name: location.name, - saturday_hours: EnrollmentHelper.localized_hours(location.saturday_hours), - state: location.state, - sunday_hours: EnrollmentHelper.localized_hours(location.sunday_hours), - weekday_hours: EnrollmentHelper.localized_hours(location.weekday_hours), - zip_code_4: location.zip_code_4, - zip_code_5: location.zip_code_5, - is_pilot: location.is_pilot, - } - end - - def localized_hours(hours) - if hours == 'Closed' - I18n.t('in_person_proofing.body.barcode.retail_hours_closed') - elsif hours.include?(' - ') # Hyphen - hours. - split(' - '). # Hyphen - map { |time| Time.zone.parse(time).strftime(I18n.t('time.formats.event_time')) }. - join(' – ') # Endash - elsif hours.include?(' – ') # Endash - hours. - split(' – '). # Endash - map { |time| Time.zone.parse(time).strftime(I18n.t('time.formats.event_time')) }. - join(' – ') # Endash - else - hours - end - end - private SECONDARY_ID_ADDRESS_MAP = { diff --git a/config/locales/en.yml b/config/locales/en.yml index 3c931ad39a4..27e0a67d630 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -717,7 +717,7 @@ errors.messages.invalid_voice_number: Invalid phone number. Check that you’ve errors.messages.missing_field: Please fill in this field. errors.messages.no_pending_profile: No profile is waiting for verification errors.messages.not_a_number: is not a number -errors.messages.otp_format: Enter your entire one-time code without spaces or special characters +errors.messages.otp_format: Enter the one-time code sent to your phone. Do not use spaces or special characters. errors.messages.password_incorrect: Incorrect password errors.messages.password_mismatch: Your passwords don’t match errors.messages.personal_key_incorrect: Incorrect personal key @@ -744,7 +744,7 @@ errors.messages.wrong_length.one: is the wrong length (should be 1 character) errors.messages.wrong_length.other: is the wrong length (should be %{count} characters) errors.piv_cac_setup.unique_name: That name is already taken. Please choose a different name. errors.registration.terms: Before you can continue, you must give us permission. Please check the box below and then click continue. -errors.sign_in.bad_password_limit: You have exceeded the maximum sign in attempts. +errors.sign_in.bad_password_limit: You have exceeded the maximum sign in attempts. You must wait %{time_left} before trying again. errors.two_factor_auth_setup.must_select_additional_option: Select an additional authentication method. errors.two_factor_auth_setup.must_select_option: Select an authentication method. errors.verify_personal_key.rate_limited: You tried too many times, please try again in %{timeout}. diff --git a/config/locales/es.yml b/config/locales/es.yml index e32ab74ce44..e0d3ee442bc 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -728,7 +728,7 @@ errors.messages.invalid_voice_number: Número de teléfono no válido. Verifique errors.messages.missing_field: Llene este campo. errors.messages.no_pending_profile: No hay ningún perfil en espera de verificación errors.messages.not_a_number: no es un número -errors.messages.otp_format: Ingrese su código de un solo uso completo, sin espacios ni caracteres especiales. +errors.messages.otp_format: Introduzca el código de un solo uso enviado a su teléfono. No utilice espacios ni caracteres especiales. errors.messages.password_incorrect: Contraseña incorrecta errors.messages.password_mismatch: Sus contraseñas no coinciden errors.messages.personal_key_incorrect: Clave personal incorrecta @@ -755,7 +755,7 @@ errors.messages.wrong_length.one: tiene la longitud incorrecta (debe ser de 1 c errors.messages.wrong_length.other: tiene la longitud incorrecta (debe ser de %{count} caracteres) errors.piv_cac_setup.unique_name: Ese nombre ya fue seleccionado. Elija un nombre diferente. errors.registration.terms: Antes de continuar, debe darnos permiso. Marque la casilla a continuación y luego haga clic en continuar. -errors.sign_in.bad_password_limit: Superó el número máximo de intentos de inicio de sesión. +errors.sign_in.bad_password_limit: Superó el número máximo de intentos de inicio de sesión. Debe esperar %{time_left} antes de volver a intentarlo. errors.two_factor_auth_setup.must_select_additional_option: Seleccione un método de autenticación adicional. errors.two_factor_auth_setup.must_select_option: Seleccione un método de autenticación. errors.verify_personal_key.rate_limited: Lo intentó demasiadas veces; vuelva a intentarlo en %{timeout}. diff --git a/config/locales/fr.yml b/config/locales/fr.yml index a70d855ed5f..c5f07b8e94d 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -717,7 +717,7 @@ errors.messages.invalid_voice_number: Numéro de téléphone non valide. Vérifi errors.messages.missing_field: Veuillez remplir ce champ. errors.messages.no_pending_profile: Aucun profil en attente de vérification errors.messages.not_a_number: n’est pas un chiffre -errors.messages.otp_format: Saisissez l’intégralité de votre code à usage unique sans espaces ni caractères spéciaux +errors.messages.otp_format: Entrez le code à usage unique envoyé sur votre téléphone. N’utilisez pas d’espaces ou de caractères spéciaux. errors.messages.password_incorrect: Mot de passe incorrect errors.messages.password_mismatch: Vos mots de passe ne correspondent pas errors.messages.personal_key_incorrect: Clé personnelle incorrecte @@ -744,7 +744,7 @@ errors.messages.wrong_length.one: n’est pas de la bonne longueur (devrait êtr errors.messages.wrong_length.other: n’est pas de la bonne longueur (devrait être de %{count} caractères) errors.piv_cac_setup.unique_name: Ce nom est déjà pris. Veuillez choisir un autre nom. errors.registration.terms: Avant de pouvoir continuer, vous devez nous donner la permission. Veuillez cocher la case ci-dessous, puis cliquez sur Suite. -errors.sign_in.bad_password_limit: Vous avez dépassé le nombre maximal de tentatives de connexion. +errors.sign_in.bad_password_limit: Vous avez dépassé le nombre maximal de tentatives de connexion. Vous devez attendre %{time_left} avant de réessayer. errors.two_factor_auth_setup.must_select_additional_option: Sélectionnez une méthode d’authentification supplémentaire. errors.two_factor_auth_setup.must_select_option: Sélectionnez une méthode d’authentification. errors.verify_personal_key.rate_limited: Vous avez essayé trop de fois, veuillez réessayer dans %{timeout}. diff --git a/config/locales/zh.yml b/config/locales/zh.yml index a8b5be89b5e..adc2d825393 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -728,7 +728,7 @@ errors.messages.invalid_voice_number: 电话号码有误。检查一下你是否 errors.messages.missing_field: 请填写这一字段。 errors.messages.no_pending_profile: 没有等待验证的用户资料 errors.messages.not_a_number: 不是数字 -errors.messages.otp_format: 输入你完整的一次性代码(没有空白或特殊字符) +errors.messages.otp_format: 输入发送到你手机的一次性代码。请勿使用空格或特殊字符。 errors.messages.password_incorrect: 密码不对。 errors.messages.password_mismatch: 你的密码不一致 errors.messages.personal_key_incorrect: 个人密钥不对 @@ -755,7 +755,7 @@ errors.messages.wrong_length.one: 长度不对(应当是 1 个字符) errors.messages.wrong_length.other: 长度不对(应当是 %{count} 个字符) errors.piv_cac_setup.unique_name: 这个名字已被使用。请选择一个不同的名字。 errors.registration.terms: 在你能继续之前,你必须授予我们你的同意。请在下面的框打勾然后点击继续。 -errors.sign_in.bad_password_limit: 你已超出登录尝试允许最多次数。 +errors.sign_in.bad_password_limit: 你已超出登录尝试允许最多次数。你必须等待 %{time_left} 才能重试。 errors.two_factor_auth_setup.must_select_additional_option: 请选择一个额外的身份证实方法。 errors.two_factor_auth_setup.must_select_option: 选择一个身份证实方法。 errors.verify_personal_key.rate_limited: 你尝试了太多次。请在 %{timeout}后再试。 diff --git a/package.json b/package.json index 279d76117b7..c0c0c50e466 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "build:css": "build-sass app/assets/stylesheets/*.css.scss app/components/*.scss --load-path=app/assets/stylesheets --out-dir=app/assets/builds" }, "dependencies": { - "@18f/identity-design-system": "^9.2.0", + "@18f/identity-design-system": "^9.3.0", "@babel/core": "^7.20.7", "@babel/preset-env": "^7.15.6", "@babel/preset-react": "^7.14.5", diff --git a/spec/controllers/concerns/idv_step_concern_spec.rb b/spec/controllers/concerns/idv_step_concern_spec.rb index b46c4ef423f..4ed251ef252 100644 --- a/spec/controllers/concerns/idv_step_concern_spec.rb +++ b/spec/controllers/concerns/idv_step_concern_spec.rb @@ -19,13 +19,6 @@ def show end describe 'before_actions' do - it 'includes handle_fraud' do - expect(idv_step_controller_class).to have_actions( - :before, - :handle_fraud, - ) - end - it 'includes check_for_mail_only_outage before_action' do expect(idv_step_controller_class).to have_actions( :before, @@ -176,6 +169,7 @@ def show describe '#confirm_letter_recently_enqueued' do controller(idv_step_controller_class) do before_action :confirm_letter_recently_enqueued + before_action :confirm_no_pending_profile end before(:each) do @@ -209,9 +203,9 @@ def show end end - describe '#confirm_no_pending_in_person_enrollment' do + describe '#confirm_no_pending_profile' do controller(idv_step_controller_class) do - before_action :confirm_no_pending_in_person_enrollment + before_action :confirm_no_pending_profile end before(:each) do @@ -245,20 +239,6 @@ def show expect(response).to redirect_to idv_in_person_ready_to_verify_url end end - end - - describe '#confirm_no_pending_gpo_profile' do - controller(idv_step_controller_class) do - before_action :confirm_no_pending_gpo_profile - end - - before(:each) do - sign_in(user) - allow(subject).to receive(:current_user).and_return(user) - routes.draw do - get 'show' => 'anonymous#show' - end - end context 'without pending gpo profile' do it 'does not redirect' do diff --git a/spec/controllers/idv/in_person/usps_locations_controller_spec.rb b/spec/controllers/idv/in_person/usps_locations_controller_spec.rb index aed47ea128e..402ae40efa4 100644 --- a/spec/controllers/idv/in_person/usps_locations_controller_spec.rb +++ b/spec/controllers/idv/in_person/usps_locations_controller_spec.rb @@ -172,32 +172,6 @@ response_status_code: nil, ) end - - context 'with non-English locale' do - let(:locale) { 'fr' } - - it 'returns content in selected locale' do - json = response.body - - expect(json).to include( - I18n.t('in_person_proofing.body.barcode.retail_hours_closed', locale: locale), - ) - I18n.locale = locale - facilities = JSON.parse(json) - - facilities.zip(locations).each do |facility, location| - expect(facility['weekday_hours']).to eq( - UspsInPersonProofing::EnrollmentHelper.localized_hours(location[:weekday_hours]), - ) - expect(facility['saturday_hours']).to eq( - UspsInPersonProofing::EnrollmentHelper.localized_hours(location[:saturday_hours]), - ) - expect(facility['sunday_hours']).to eq( - UspsInPersonProofing::EnrollmentHelper.localized_hours(location[:sunday_hours]), - ) - end - end - end end context 'with a timeout from Faraday' do diff --git a/spec/controllers/idv/personal_key_controller_spec.rb b/spec/controllers/idv/personal_key_controller_spec.rb index 2899e92299c..8b3ba37bef2 100644 --- a/spec/controllers/idv/personal_key_controller_spec.rb +++ b/spec/controllers/idv/personal_key_controller_spec.rb @@ -180,7 +180,7 @@ def assert_personal_key_generated_for_profiles(*profile_pii_pairs) :confirm_idv_needed, :confirm_personal_key_acknowledged_if_needed, :confirm_no_pending_in_person_enrollment, - :handle_fraud, + :confirm_no_pending_profile, ) end diff --git a/spec/controllers/users/sessions_controller_spec.rb b/spec/controllers/users/sessions_controller_spec.rb index e50c63953fe..2495cf64fcc 100644 --- a/spec/controllers/users/sessions_controller_spec.rb +++ b/spec/controllers/users/sessions_controller_spec.rb @@ -116,6 +116,35 @@ end end + context 'locked out session' do + let(:locked_at) { Time.zone.now } + let(:user) { create(:user, :fully_registered) } + let(:bad_password_window) { IdentityConfig.store.max_bad_passwords_window_in_seconds } + + before do + session[:bad_password_count] = IdentityConfig.store.max_bad_passwords + 1 + session[:max_bad_passwords_at] = locked_at.to_i + end + + it 'renders an error letting user know they are locked out for a period of time' do + post :create, params: { user: { email: user.email.upcase, password: user.password } } + current_time = Time.zone.now + time_in_hours = distance_of_time_in_words( + current_time, + (locked_at + bad_password_window.seconds), + true, + ) + + expect(response).to redirect_to root_url + expect(flash[:error]).to eq( + t( + 'errors.sign_in.bad_password_limit', + time_left: time_in_hours, + ), + ) + end + end + it 'tracks the unsuccessful authentication for existing user' do user = create(:user, :fully_registered) @@ -157,13 +186,6 @@ post :create, params: { user: { email: 'foo@example.com', password: 'password' } } end - it 'tracks unsuccessful authentication for too many auth failures' do - allow(subject).to receive(:session_bad_password_count_max_exceeded?).and_return(true) - mock_email_parameter = { email: 'bob@example.com' } - - post :create, params: { user: { **mock_email_parameter, password: 'eatCake!' } } - end - it 'tracks unsuccessful authentication for locked out user' do user = create( :user, diff --git a/spec/features/idv/verify_by_mail_pending_spec.rb b/spec/features/idv/verify_by_mail_pending_spec.rb new file mode 100644 index 00000000000..dee065f1dff --- /dev/null +++ b/spec/features/idv/verify_by_mail_pending_spec.rb @@ -0,0 +1,39 @@ +require 'rails_helper' + +RSpec.feature 'a user that is pending verify by mail', allowed_extra_analytics: [:*] do + include IdvStepHelper + + it 'requires them to enter code or cancel to enter the proofing flow' do + user = create(:user, :fully_registered) + profile = create(:profile, :with_pii, :verify_by_mail_pending, user: user) + create(:gpo_confirmation_code, profile: profile, created_at: 2.days.ago, updated_at: 2.days.ago) + + start_idv_from_sp(biometric_comparison_required: false) + sign_in_live_with_2fa(user) + + expect(current_path).to eq(idv_verify_by_mail_enter_code_path) + + # Attempting to start IdV should require enter-code to be completed + visit idv_welcome_path + expect(current_path).to eq(idv_verify_by_mail_enter_code_path) + + # Cancelling redirects to IdV flow start + click_on t('idv.gpo.address_accordion.cta_link') + click_idv_continue + + expect(current_path).to eq(idv_welcome_path) + end + + it 'does not require them to enter their code if they are upgrading to biometric' do + user = create(:user, :fully_registered) + profile = create(:profile, :with_pii, :verify_by_mail_pending, user: user) + create(:gpo_confirmation_code, profile: profile, created_at: 2.days.ago, updated_at: 2.days.ago) + + start_idv_from_sp(biometric_comparison_required: true) + sign_in_live_with_2fa(user) + + # The user is redirected to proofing since their pending profile does not meet + # the biometric comparison requirement + expect(current_path).to eq(idv_welcome_path) + end +end diff --git a/spec/features/visitors/bad_password_spec.rb b/spec/features/visitors/bad_password_spec.rb index 53fe0364040..1493b96a04c 100644 --- a/spec/features/visitors/bad_password_spec.rb +++ b/spec/features/visitors/bad_password_spec.rb @@ -1,8 +1,10 @@ require 'rails_helper' RSpec.feature 'Visitor signs in with bad passwords and gets locked out' do + include ActionView::Helpers::DateHelper let(:user) { create(:user, :fully_registered) } let(:bad_password) { 'badpassword' } + let(:window) { IdentityConfig.store.max_bad_passwords_window_in_seconds.seconds } scenario 'visitor tries too many bad passwords gets locked out then waits window seconds' do visit new_user_session_path @@ -15,14 +17,33 @@ expect(page).to have_content(error_message) expect(page).to have_current_path(new_user_session_path) end + locked_at = Time.zone.at(page.get_rack_session['max_bad_passwords_at']) + # Need to do this because getting rack session changes the url. + visit new_user_session_path 2.times do fill_in_credentials_and_submit(user.email, bad_password) + expect(page).to have_current_path(new_user_session_path) - expect(page).to have_content(t('errors.sign_in.bad_password_limit')) + new_time = Time.zone.at(locked_at) + window + time_left = distance_of_time_in_words(Time.zone.now, new_time, true) + expect(page).to have_content( + t( + 'errors.sign_in.bad_password_limit', + time_left: time_left, + ), + ) end fill_in_credentials_and_submit(user.email, user.password) expect(page).to have_current_path(new_user_session_path) - expect(page).to have_content(t('errors.sign_in.bad_password_limit')) + new_time = Time.zone.at(locked_at) + window + time_left = distance_of_time_in_words(Time.zone.now, new_time, true) + expect(page).to have_content( + t( + 'errors.sign_in.bad_password_limit', + time_left: time_left, + ), + ) + travel_to(IdentityConfig.store.max_bad_passwords_window_in_seconds.seconds.from_now) do fill_in_credentials_and_submit(user.email, bad_password) expect(page).to have_content(error_message) diff --git a/yarn.lock b/yarn.lock index 24ae052d682..1bacc1cdecc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,13 +2,13 @@ # yarn lockfile v1 -"@18f/identity-design-system@^9.2.0": - version "9.2.0" - resolved "https://registry.yarnpkg.com/@18f/identity-design-system/-/identity-design-system-9.2.0.tgz#36f1e4c4c68cae52c0cd4d5256ac33e6ef872763" - integrity sha512-gzzcRtxRPKdxcbdgYKBS+IEmBielCwbxB9KkUTwNyXTqDMJoWmscSODPEpmegIEB8Tg/LXwxJQfr+LyEePYewQ== +"@18f/identity-design-system@^9.3.0": + version "9.3.0" + resolved "https://registry.yarnpkg.com/@18f/identity-design-system/-/identity-design-system-9.3.0.tgz#990089ea93c5ac773cf87ac61d175d739ec89ae7" + integrity sha512-vOhBNu20rtSUUIx0bpyLjUTcOzyzLpq9/Oyot+vWPYxyatTg2m+7dpQK8xSNVDtnNtpVEwOhf1GnOGF+v7n+nA== dependencies: "@types/uswds__uswds" "^3.8.0" - "@uswds/uswds" "^3.8.0" + "@uswds/uswds" "^3.8.1" "@aashutoshrathi/word-wrap@^1.2.3": version "1.2.6" @@ -1895,10 +1895,10 @@ "@typescript-eslint/types" "6.7.5" eslint-visitor-keys "^3.4.1" -"@uswds/uswds@^3.8.0": - version "3.8.0" - resolved "https://registry.yarnpkg.com/@uswds/uswds/-/uswds-3.8.0.tgz#dba0b0b38182053779276f9ae6809474bd31d548" - integrity sha512-rMwCXe/u4HGkfskvS1Iuabapi/EXku3ChaIFW7y/dUhc7R1TXQhbbfp8YXEjmXPF0yqJnv9T08WPgS0fQqWZ8w== +"@uswds/uswds@^3.8.1": + version "3.8.1" + resolved "https://registry.yarnpkg.com/@uswds/uswds/-/uswds-3.8.1.tgz#3d834559498ae1bb7d3a618f3f85a5f4e9818497" + integrity sha512-bKG/B9mJF1v0yoqth48wQDzST5Xyu3OxxpePIPDyhKWS84oDrCehnu3Z88JhSjdIAJMl8dtjtH8YvdO9kZUpAg== dependencies: classlist-polyfill "1.2.0" object-assign "4.1.1"