diff --git a/.eslintrc b/.eslintrc index a9838da95fd..d44fdbcb02b 100644 --- a/.eslintrc +++ b/.eslintrc @@ -40,6 +40,8 @@ 'app/radio-btn', 'app/print-personal-key', 'app/utils/ms-formatter', + 'app/phone-internationalization', + 'app/i18n-dropdown', ], } } diff --git a/.reek b/.reek index 80ef814f099..d3182612dcd 100644 --- a/.reek +++ b/.reek @@ -14,6 +14,7 @@ DuplicateMethodCall: - UserFlowExporter#self.massage_assets FeatureEnvy: exclude: + - ActiveJob::Logging::LogSubscriber#json_for - track_registration - append_info_to_payload - generate_slo_request @@ -24,6 +25,8 @@ FeatureEnvy: - Pii::Attributes#[]= - OpenidConnectLogoutForm#load_identity - Idv::ProfileMaker#pii_from_applicant + - Idv::Step#vendor_validator_result + - IdvSession#vendor_result_timed_out? InstanceVariableAssumption: exclude: - User @@ -41,6 +44,8 @@ NilCheck: LongParameterList: exclude: - IdentityLinker#optional_attributes + - VendorValidatorJob#perform + - Idv::VendorResult#initialize RepeatedConditional: exclude: - Users::ResetPasswordsController @@ -53,6 +58,7 @@ TooManyInstanceVariables: exclude: - OpenidConnectAuthorizeForm - OpenidConnectRedirector + - Idv::VendorResult TooManyStatements: max_statements: 6 exclude: @@ -72,6 +78,7 @@ TooManyMethods: - OpenidConnect::AuthorizationController - Idv::Session - User + - Verify::SessionsController UncommunicativeMethodName: exclude: - PhoneConfirmationFlow @@ -89,6 +96,7 @@ UtilityFunction: public_methods_only: true exclude: - AnalyticsEventJob#perform + - ApplicationController#default_url_options - ApplicationHelper#step_class - PersonalKeyFormatter#regexp - SessionTimeoutWarningHelper#frequency @@ -97,6 +105,9 @@ UtilityFunction: - SessionDecorator - WorkerHealthChecker::Middleware#call - UserEncryptedAttributeOverrides#create_fingerprint + - LocaleHelper#locale_url_param + - Verify::Base#mock_vendor_partial + - IdvSession#timed_out_vendor_error 'app/controllers': InstanceVariableAssumption: enabled: false diff --git a/.rubocop.yml b/.rubocop.yml index b2afe411f82..3adaebd80a6 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -47,6 +47,7 @@ Metrics/BlockLength: - 'config/initializers/secure_headers.rb' - 'config/routes.rb' - 'spec/**/*.rb' + - 'config/initializers/active_job_logger_patch.rb' Metrics/ClassLength: Description: Avoid classes longer than 100 lines of code. @@ -93,6 +94,9 @@ Metrics/ModuleLength: - spec/**/* - 'app/controllers/concerns/two_factor_authenticatable.rb' +Metrics/ParameterLists: + CountKeywordArgs: false + # This is a Rails 5 feature, so it should be disabled until we upgrade Rails/HttpPositionalArguments: Description: 'Use keyword arguments instead of positional arguments in http method calls.' diff --git a/Gemfile b/Gemfile index d92ebf701ed..cebbb9caaeb 100644 --- a/Gemfile +++ b/Gemfile @@ -24,8 +24,10 @@ gem 'http_accept_language' gem 'httparty' gem 'json-jwt' gem 'lograge' +gem 'net-sftp' gem 'newrelic_rpm' gem 'pg' +gem 'phonelib' gem 'phony_rails' gem 'premailer-rails' gem 'proofer', github: '18F/identity-proofer-gem', branch: 'master' @@ -113,4 +115,5 @@ end group :production do gem 'equifax', git: 'git@github.com:18F/identity-equifax-api-client-gem.git', branch: 'master' + gem 'mandrill_dm' end diff --git a/Gemfile.lock b/Gemfile.lock index 69f760e3ded..5597dc22c35 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,6 @@ GIT remote: git@github.com:18F/identity-equifax-api-client-gem.git - revision: 4308a502baf7b65e8b463ecafc2d428d530b4349 + revision: 889aad815bda2ff2a41cd2b108e2afae7f50d8b8 branch: master specs: equifax (1.0.0) @@ -148,7 +148,7 @@ GEM bullet (5.5.1) activesupport (>= 3.0.0) uniform_notifier (~> 1.10.0) - bummr (0.2.0) + bummr (0.1.8) rainbow thor byebug (9.0.6) @@ -247,6 +247,7 @@ GEM errbase (0.0.3) erubis (2.7.0) eventmachine (1.0.9.1) + excon (0.57.0) execjs (2.7.0) factory_girl (4.8.0) activesupport (>= 3.0.0) @@ -300,7 +301,7 @@ GEM httpi (2.4.2) rack socksify - i18n (0.8.4) + i18n (0.8.6) i18n-tasks (0.9.15) activesupport (>= 4.0.2) ast (>= 2.1.0) @@ -314,7 +315,7 @@ GEM ice_nine (0.11.2) iniparse (1.4.2) jmespath (1.3.1) - json (2.1.0) + json (1.8.6) json-jwt (1.7.2) activesupport bindata @@ -348,6 +349,12 @@ GEM skinny (~> 0.2.3) sqlite3 (~> 1.3) thin (~> 1.5.0) + mandrill-api (1.0.53) + excon (>= 0.16.0, < 1.0) + json (>= 1.7.7, < 2.0) + mandrill_dm (1.3.4) + mail (~> 2.6) + mandrill-api (~> 1.0.53) memory_profiler (0.9.8) method_source (0.8.2) mime-types (3.1) @@ -360,6 +367,8 @@ GEM nenv (0.3.0) net-scp (1.2.1) net-ssh (>= 2.6.5) + net-sftp (2.1.2) + net-ssh (>= 2.6.5) net-ssh (4.1.0) newrelic_rpm (4.2.0.334) nokogiri (1.8.0) @@ -376,6 +385,7 @@ GEM parser (2.4.0.0) ast (~> 2.2) pg (0.21.0) + phonelib (0.6.12) phony (2.15.44) phony_rails (0.14.6) activesupport (>= 3.0) @@ -696,9 +706,12 @@ DEPENDENCIES json-jwt lograge mailcatcher + mandrill_dm + net-sftp newrelic_rpm overcommit pg + phonelib phony_rails poltergeist premailer-rails diff --git a/app/assets/images/globe-blue.svg b/app/assets/images/globe-blue.svg new file mode 100644 index 00000000000..d72989b85bc --- /dev/null +++ b/app/assets/images/globe-blue.svg @@ -0,0 +1 @@ +globe-blue \ No newline at end of file diff --git a/app/assets/images/globe-white.svg b/app/assets/images/globe-white.svg new file mode 100644 index 00000000000..34829ec0c2e --- /dev/null +++ b/app/assets/images/globe-white.svg @@ -0,0 +1 @@ +globe-white \ No newline at end of file diff --git a/app/assets/images/sp-logos/cbp-ttp.png b/app/assets/images/sp-logos/cbp-ttp.png new file mode 100644 index 00000000000..2beaf6fc93a Binary files /dev/null and b/app/assets/images/sp-logos/cbp-ttp.png differ diff --git a/app/assets/images/spinner.gif b/app/assets/images/spinner.gif new file mode 100644 index 00000000000..913ce4c9165 Binary files /dev/null and b/app/assets/images/spinner.gif differ diff --git a/app/assets/javascripts/app/components/accordion.js b/app/assets/javascripts/app/components/accordion.js index 2724585c6e5..14f5da6239d 100644 --- a/app/assets/javascripts/app/components/accordion.js +++ b/app/assets/javascripts/app/components/accordion.js @@ -72,6 +72,7 @@ class Accordion extends Events { this.content.classList.add('shown'); this.content.classList.remove('animate-out'); this.content.classList.add('animate-in'); + this.content.setAttribute('aria-hidden', 'false'); this.emit('accordion.show'); } @@ -81,6 +82,7 @@ class Accordion extends Events { this.shownIcon.classList.add('display-none'); this.content.classList.remove('animate-in'); this.content.classList.add('animate-out'); + this.content.setAttribute('aria-hidden', 'true'); this.emit('accordion.hide'); this.header.focus(); } diff --git a/app/assets/javascripts/app/form-field-format.js b/app/assets/javascripts/app/form-field-format.js index 308d63cf687..f00ec070241 100644 --- a/app/assets/javascripts/app/form-field-format.js +++ b/app/assets/javascripts/app/form-field-format.js @@ -1,5 +1,6 @@ -import { PhoneFormatter, SocialSecurityNumberFormatter, TextField } from 'field-kit'; +import { SocialSecurityNumberFormatter, TextField } from 'field-kit'; import DateFormatter from './modules/date-formatter'; +import InternationalPhoneFormatter from './modules/international-phone-formatter'; import NumericFormatter from './modules/numeric-formatter'; import PersonalKeyFormatter from './modules/personal-key-formatter'; import ZipCodeFormatter from './modules/zip-code-formatter'; @@ -13,7 +14,7 @@ function formatForm() { ['.home_equity_line', new NumericFormatter()], ['.mfa', new NumericFormatter()], ['.mortgage', new NumericFormatter()], - ['.phone', new PhoneFormatter()], + ['.phone', new InternationalPhoneFormatter()], ['.personal-key', new PersonalKeyFormatter()], ['.ssn', new SocialSecurityNumberFormatter()], ['.zipcode', new ZipCodeFormatter()], diff --git a/app/assets/javascripts/app/form-validation.js b/app/assets/javascripts/app/form-validation.js index 7d82dec7729..7d534d6e6c0 100644 --- a/app/assets/javascripts/app/form-validation.js +++ b/app/assets/javascripts/app/form-validation.js @@ -10,7 +10,7 @@ document.addEventListener('DOMContentLoaded', () => { if (input) { input.addEventListener('input', () => { if (input.validity.patternMismatch) { - input.setCustomValidity(I18n.t(`idv.errors.pattern_mismatch.${f}`)); + input.setCustomValidity(I18n.t(`idv.errors.pattern_mismatch.${I18n.key(f)}`)); } else { input.setCustomValidity(''); } diff --git a/app/assets/javascripts/app/i18n-dropdown.js b/app/assets/javascripts/app/i18n-dropdown.js new file mode 100644 index 00000000000..431765f0f47 --- /dev/null +++ b/app/assets/javascripts/app/i18n-dropdown.js @@ -0,0 +1,18 @@ +import 'classlist.js'; + +document.addEventListener('DOMContentLoaded', () => { + const mobileLink = document.querySelector('.i18n-mobile-toggle'); + const mobileDropdown = document.querySelector('.i18n-mobile-dropdown'); + const desktopLink = document.querySelector('.i18n-desktop-toggle'); + const desktopDropdown = document.querySelector('.i18n-desktop-dropdown'); + + function initDropdown (trigger, dropdown) { + trigger.addEventListener('click', function() { + this.classList.toggle('focused'); + dropdown.classList.toggle('focused'); + }); + } + + if (mobileLink) initDropdown(mobileLink, mobileDropdown); + if (desktopLink) initDropdown(desktopLink, desktopDropdown); +}); diff --git a/app/assets/javascripts/app/modules/international-phone-formatter.js b/app/assets/javascripts/app/modules/international-phone-formatter.js new file mode 100644 index 00000000000..2b3ee0b47db --- /dev/null +++ b/app/assets/javascripts/app/modules/international-phone-formatter.js @@ -0,0 +1,69 @@ +import { Formatter } from 'field-kit'; +import { asYouType as AsYouType } from 'libphonenumber-js'; + +const fixCountryCodeSpacing = (text, countryCode) => { + // If the text is `+123456`, make it `+123 456` + if (text[countryCode.length + 1] !== ' ') { + return text.replace(`+${countryCode}`, `+${countryCode} `); + } + return text; +}; + +const getFormattedTextData = (text) => { + if (text === '1') { + text = '+1'; + } + + const asYouType = new AsYouType('US'); + let formattedText = asYouType.input(text); + const countryCode = asYouType.country_phone_code; + + if (asYouType.country_phone_code) { + formattedText = fixCountryCodeSpacing(formattedText, countryCode); + } + + return { + text: formattedText, + template: asYouType.template, + countryCode, + }; +}; + +const cursorPosition = (formattedTextData) => { + // If the text is `(23 )` the cursor goes after the 3 + const match = formattedTextData.text.match(/\d[^\d]*$/); + if (match) { + return match.index + 1; + } + return formattedTextData.text.length + 1; +}; + +class InternationalPhoneFormatter extends Formatter { + format(text) { + const formattedTextData = getFormattedTextData(text); + return super.format(formattedTextData.text); + } + + // eslint-disable-next-line class-methods-use-this + parse(text) { + return text.replace(/[^\d+]/g, ''); + } + + isChangeValid(change, error) { + const formattedTextData = getFormattedTextData(change.proposed.text); + const previousFormattedTextData = getFormattedTextData(change.current.text); + + if (previousFormattedTextData.template && + !formattedTextData.template && + change.inserted.text.length === 1 + ) { + return false; + } + + change.proposed.text = formattedTextData.text; + change.proposed.selectedRange.start = cursorPosition(formattedTextData); + return super.isChangeValid(change, error); + } +} + +export default InternationalPhoneFormatter; diff --git a/app/assets/javascripts/app/phone-internationalization.js b/app/assets/javascripts/app/phone-internationalization.js new file mode 100644 index 00000000000..af3e4724a33 --- /dev/null +++ b/app/assets/javascripts/app/phone-internationalization.js @@ -0,0 +1,136 @@ +import { PhoneFormatter } from 'field-kit'; + +const INTERNATIONAL_CODE_REGEX = /^\+(\d+) |^1 /; + +const I18n = window.LoginGov.I18n; +const phoneFormatter = new PhoneFormatter(); + +const getPhoneUnsupportedAreaCodeCountry = (areaCode) => { + const form = document.querySelector('#new_two_factor_setup_form'); + const phoneUnsupportedAreaCodes = JSON.parse(form.dataset.unsupportedAreaCodes); + return phoneUnsupportedAreaCodes[areaCode]; +}; + +const areaCodeFromUSPhone = (phone) => { + const digits = phoneFormatter.digitsWithoutCountryCode(phone); + if (digits.length >= 10) { + return digits.slice(0, 3); + } + return null; +}; + +const selectedInternationCodeOption = () => { + const dropdown = document.querySelector('[data-international-phone-form] .international-code'); + return dropdown.item(dropdown.selectedIndex); +}; + +const unsupportedUSPhoneOTPDeliveryWarningMessage = (phone) => { + const areaCode = areaCodeFromUSPhone(phone); + const country = getPhoneUnsupportedAreaCodeCountry(areaCode); + if (country) { + const messageTemplate = I18n.t('devise.two_factor_authentication.otp_delivery_preference.phone_unsupported'); + return messageTemplate.replace('%{location}', country); + } + return null; +}; + +const unsupportedInternationalPhoneOTPDeliveryWarningMessage = () => { + const selectedOption = selectedInternationCodeOption(); + if (selectedOption.dataset.smsOnly === 'true') { + const messageTemplate = I18n.t('devise.two_factor_authentication.otp_delivery_preference.phone_unsupported'); + return messageTemplate.replace('%{location}', selectedOption.dataset.countryName); + } + return null; +}; + +const unsupportedPhoneOTPDeliveryWarningMessage = (phone) => { + const internationCodeOption = selectedInternationCodeOption(); + if (internationCodeOption.dataset.countryCode === '1') { + return unsupportedUSPhoneOTPDeliveryWarningMessage(phone); + } + return unsupportedInternationalPhoneOTPDeliveryWarningMessage(); +}; + +const updateOTPDeliveryMethods = () => { + const phoneRadio = document.querySelector('#two_factor_setup_form_otp_delivery_preference_voice'); + const smsRadio = document.querySelector('#two_factor_setup_form_otp_delivery_preference_sms'); + + if (!phoneRadio || !smsRadio) { + return; + } + + const phoneInput = document.querySelector('[data-international-phone-form] .phone'); + const phoneLabel = phoneRadio.parentNode.parentNode; + const deliveryMethodHint = document.querySelector('#otp_delivery_preference_instruction'); + const optPhoneLabelInfo = document.querySelector('#otp_phone_label_info'); + + const phone = phoneInput.value; + + const warningMessage = unsupportedPhoneOTPDeliveryWarningMessage(phone); + if (warningMessage) { + phoneRadio.disabled = true; + phoneLabel.classList.add('btn-disabled'); + smsRadio.click(); + deliveryMethodHint.innerText = warningMessage; + optPhoneLabelInfo.innerText = I18n.t('devise.two_factor_authentication.otp_phone_label_info_modile_only'); + } else { + phoneRadio.disabled = false; + phoneLabel.classList.remove('btn-disabled'); + deliveryMethodHint.innerText = I18n.t('devise.two_factor_authentication.otp_delivery_preference.instruction'); + optPhoneLabelInfo.innerText = I18n.t('devise.two_factor_authentication.otp_phone_label_info'); + } +}; + +const internationalCodeFromPhone = (phone) => { + const match = phone.match(INTERNATIONAL_CODE_REGEX); + if (match) { + return match[1] || match[2]; + } + return '1'; +}; + +const updateInternationalCodeSelection = () => { + const phoneInput = document.querySelector('[data-international-phone-form] .phone'); + const phone = phoneInput.value; + const internationalCode = internationalCodeFromPhone(phone); + const option = document.querySelector(`[data-country-code='${internationalCode}']`); + if (option) { + const dropdown = document.querySelector('[data-international-phone-form] .international-code'); + dropdown.value = option.value; + } +}; + +const updateInternationalCodeInPhone = (phone, newCode) => { + if (phone.match(/^\+[^d+]$/)) { + phone = phone.replace(/^\+[^d+]$/, ''); + } + if (phone.match(INTERNATIONAL_CODE_REGEX)) { + return phone.replace(INTERNATIONAL_CODE_REGEX, `+${newCode} `); + } + return `+${newCode} ${phone}`; +}; + +const updateInternationalCodeInput = () => { + const phoneInput = document.querySelector('[data-international-phone-form] .phone'); + const phone = phoneInput.value; + const inputInternationalCode = internationalCodeFromPhone(phone); + const selectedInternationalCode = selectedInternationCodeOption().dataset.countryCode; + + if (inputInternationalCode !== selectedInternationalCode) { + phoneInput.value = updateInternationalCodeInPhone(phone, selectedInternationalCode); + } +}; + +document.addEventListener('DOMContentLoaded', () => { + const phoneInput = document.querySelector('[data-international-phone-form] .phone'); + const codeInput = document.querySelector('[data-international-phone-form] .international-code'); + if (phoneInput) { + phoneInput.addEventListener('keyup', updateOTPDeliveryMethods); + phoneInput.addEventListener('keyup', updateInternationalCodeSelection); + } + if (codeInput) { + codeInput.addEventListener('change', updateOTPDeliveryMethods); + codeInput.addEventListener('change', updateInternationalCodeInput); + updateOTPDeliveryMethods(); + } +}); diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 0f4ba9ccf44..64d07a2fd48 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -5,4 +5,6 @@ import 'app/form-validation'; import 'app/form-field-format'; import 'app/idv-finance-helper'; import 'app/radio-btn'; +import 'app/phone-internationalization'; import 'app/print-personal-key'; +import 'app/i18n-dropdown'; diff --git a/app/assets/javascripts/misc/i18n-strings.js.erb b/app/assets/javascripts/misc/i18n-strings.js.erb index 4c5fa4c198d..a2bd235c4c7 100644 --- a/app/assets/javascripts/misc/i18n-strings.js.erb +++ b/app/assets/javascripts/misc/i18n-strings.js.erb @@ -1,11 +1,15 @@ window.LoginGov = window.LoginGov || {}; <% keys = [ + 'devise.two_factor_authentication.otp_delivery_preference.instruction', + 'devise.two_factor_authentication.otp_delivery_preference.phone_unsupported', + 'devise.two_factor_authentication.otp_phone_label_info', + 'devise.two_factor_authentication.otp_phone_label_info_modile_only', 'errors.messages.format_mismatch', 'errors.messages.missing_field', 'forms.passwords.show', 'idv.errors.pattern_mismatch.dob', - 'idv.errors.pattern_mismatch.personal-key', + 'idv.errors.pattern_mismatch.personal_key', 'idv.errors.pattern_mismatch.ssn', 'idv.errors.pattern_mismatch.zipcode', 'idv.modal.button.warning', @@ -16,40 +20,45 @@ window.LoginGov = window.LoginGov || {}; 'instructions.password.strength.v', 'links.remove', 'valid_email.validations.email.invalid', - 'zxcvbn.feedback.Use a few words, avoid common phrases', - 'zxcvbn.feedback.No need for symbols, digits, or uppercase letters', - 'zxcvbn.feedback.Add another word or two_ Uncommon words are better_', - 'zxcvbn.feedback.Straight rows of keys are easy to guess', - 'zxcvbn.feedback.Short keyboard patterns are easy to guess', - 'zxcvbn.feedback.Use a longer keyboard pattern with more turns', - 'zxcvbn.feedback.Repeats like "aaa" are easy to guess', - 'zxcvbn.feedback.Repeats like "abcabcabc" are only slightly harder to guess than "abc"', - 'zxcvbn.feedback.Avoid repeated words and characters', - 'zxcvbn.feedback.Sequences like abc or 6543 are easy to guess', - 'zxcvbn.feedback.Avoid sequences', - 'zxcvbn.feedback.Recent years are easy to guess', - 'zxcvbn.feedback.Avoid recent years', - 'zxcvbn.feedback.Avoid years that are associated with you', - 'zxcvbn.feedback.Dates are often easy to guess', - 'zxcvbn.feedback.Avoid dates and years that are associated with you', - 'zxcvbn.feedback.This is a top-10 common password', - 'zxcvbn.feedback.This is a top-100 common password', - 'zxcvbn.feedback.This is a very common password', - 'zxcvbn.feedback.This is similar to a commonly used password', - 'zxcvbn.feedback.A word by itself is easy to guess', - 'zxcvbn.feedback.Names and surnames by themselves are easy to guess', - 'zxcvbn.feedback.Common names and surnames are easy to guess', - 'zxcvbn.feedback.Capitalization doesn\'t help very much', - 'zxcvbn.feedback.All-uppercase is almost as easy to guess as all-lowercase', - 'zxcvbn.feedback.Reversed words aren\'t much harder to guess', - 'zxcvbn.feedback.Predictable substitutions like \'@\' instead of \'a\' don\'t help very much' + 'zxcvbn.feedback.a_word_by_itself_is_easy_to_guess', + 'zxcvbn.feedback.add_another_word_or_two_uncommon_words_are_better', + 'zxcvbn.feedback.all_uppercase_is_almost_as_easy_to_guess_as_all_lowercase', + 'zxcvbn.feedback.avoid_dates_and_years_that_are_associated_with_you', + 'zxcvbn.feedback.avoid_recent_years', + 'zxcvbn.feedback.avoid_repeated_words_and_characters', + 'zxcvbn.feedback.avoid_sequences', + 'zxcvbn.feedback.avoid_years_that_are_associated_with_you', + 'zxcvbn.feedback.capitalization_doesnt_help_very_much', + 'zxcvbn.feedback.common_names_and_surnames_are_easy_to_guess', + 'zxcvbn.feedback.dates_are_often_easy_to_guess', + 'zxcvbn.feedback.names_and_surnames_by_themselves_are_easy_to_guess', + 'zxcvbn.feedback.there_is_no_need_for_symbols_digits_or_uppercase_letters', + 'zxcvbn.feedback.predictable_substitutions_like__instead_of_a_dont_help_very_much', + 'zxcvbn.feedback.recent_years_are_easy_to_guess', + 'zxcvbn.feedback.repeats_like_aaa_are_easy_to_guess', + 'zxcvbn.feedback.repeats_like_abcabcabc_are_only_slightly_harder_to_guess_than_abc', + 'zxcvbn.feedback.reversed_words_arent_much_harder_to_guess', + 'zxcvbn.feedback.sequences_like_abc_or_6543_are_easy_to_guess', + 'zxcvbn.feedback.short_keyboard_patterns_are_easy_to_guess', + 'zxcvbn.feedback.straight_rows_of_keys_are_easy_to_guess', + 'zxcvbn.feedback.this_is_a_top_10_common_password', + 'zxcvbn.feedback.this_is_a_top_100_common_password', + 'zxcvbn.feedback.this_is_a_very_common_password', + 'zxcvbn.feedback.this_is_similar_to_a_commonly_used_password', + 'zxcvbn.feedback.for_a_stronger_password_use_a_few_words_separated_by_spaces_but_avoid_common_phrases', + 'zxcvbn.feedback.use_a_longer_keyboard_pattern_with_more_turns' ] %> window.LoginGov.I18n = { + currentLocale: function() { return this.__currentLocale || (this.__currentLocale = document.querySelector('html').lang); }, strings: {}, - t: function(key) { return this.strings[key]; } + t: function(key) { return this.strings[this.currentLocale()][key]; }, + key: function(key) { return key.replace(/[ -]/g, '_').replace(/\W/g, '').toLowerCase(); } }; -<% keys.each do |key| %> -window.LoginGov.I18n.strings['<%= ActionController::Base.helpers.j key %>'] = '<%= ActionController::Base.helpers.j I18n.t(key) %>'; +<% I18n.available_locales.each do |locale| %> + window.LoginGov.I18n.strings['<%= ActionController::Base.helpers.j locale.to_s %>'] = {}; + <% keys.each do |key| %> + window.LoginGov.I18n.strings['<%= ActionController::Base.helpers.j locale.to_s %>']['<%= ActionController::Base.helpers.j key %>'] = '<%= ActionController::Base.helpers.j I18n.t(key, locale: locale) %>'; + <% end %> <% end %> diff --git a/app/assets/javascripts/misc/pw-strength.js b/app/assets/javascripts/misc/pw-strength.js index ae0b444be1b..82c0a79c92a 100644 --- a/app/assets/javascripts/misc/pw-strength.js +++ b/app/assets/javascripts/misc/pw-strength.js @@ -27,8 +27,7 @@ function getFeedback(z) { const { warning, suggestions } = z.feedback; function lookup(str) { - const strFormatted = str.replace(/\./g, '_'); - return I18n.t(`zxcvbn.feedback.${strFormatted}`); + return I18n.t(`zxcvbn.feedback.${I18n.key(str)}`); } if (!warning && !suggestions.length) return ''; diff --git a/app/assets/stylesheets/components/_btn.scss b/app/assets/stylesheets/components/_btn.scss index 297d879d781..5f7c5d2f972 100644 --- a/app/assets/stylesheets/components/_btn.scss +++ b/app/assets/stylesheets/components/_btn.scss @@ -72,3 +72,9 @@ outline: none; } } + +.btn-disabled { + background-color: $gray-light; + border-color: $gray; + color: $gray; +} diff --git a/app/assets/stylesheets/components/_footer.scss b/app/assets/stylesheets/components/_footer.scss index f352278c144..4b9e8042f51 100644 --- a/app/assets/stylesheets/components/_footer.scss +++ b/app/assets/stylesheets/components/_footer.scss @@ -15,6 +15,7 @@ html { .footer { flex: none; // 2 + position: relative; } .site-wrap { diff --git a/app/assets/stylesheets/components/_form.scss b/app/assets/stylesheets/components/_form.scss index cf60f39a954..71df7c2435c 100644 --- a/app/assets/stylesheets/components/_form.scss +++ b/app/assets/stylesheets/components/_form.scss @@ -152,6 +152,11 @@ input::-webkit-inner-spin-button { background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNy4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB2aWV3Qm94PSIwIDAgOCA4IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCA4IDgiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPHBhdGggZmlsbD0iI0ZGRkZGRiIgZD0iTTQsMUMyLjMsMSwxLDIuMywxLDRzMS4zLDMsMywzczMtMS4zLDMtM1M1LjcsMSw0LDF6Ii8+DQo8L3N2Zz4NCg==); } +.radio input:disabled ~ .indicator { + background-color: $gray-light; + border-color: $gray; +} + .select-alt { color: $white; display: inline-block; diff --git a/app/assets/stylesheets/components/_i18n-dropdown.scss b/app/assets/stylesheets/components/_i18n-dropdown.scss new file mode 100644 index 00000000000..317a9c0face --- /dev/null +++ b/app/assets/stylesheets/components/_i18n-dropdown.scss @@ -0,0 +1,47 @@ +.i18n-mobile-toggle, +.i18n-desktop-toggle { + cursor: pointer; + + &.focused .caret { + transform: rotateX(180deg) translateY(-1px); + } +} + +.i18n-mobile-dropdown, +.i18n-desktop-dropdown { + &.focused { + display: block; + } +} + +.i18n-mobile-dropdown { + background-color: $blue-light; + bottom: 100%; + display: none; + left: 0; + position: absolute; + right: 0; +} + +.i18n-desktop-toggle { + position: relative; + text-decoration: none; + + &.focused { + background-color: $blue; + border-radius: 0; + margin-bottom: 0; + margin-top: 0; + padding-bottom: 12px; + padding-top: 12px; + } +} + +.i18n-desktop-dropdown { + background-color: $blue; + bottom: 100%; + display: none; + left: -1px; + position: absolute; + width: 194px; +} diff --git a/app/assets/stylesheets/components/_loading.scss b/app/assets/stylesheets/components/_loading.scss new file mode 100644 index 00000000000..9a2e669ae3a --- /dev/null +++ b/app/assets/stylesheets/components/_loading.scss @@ -0,0 +1,4 @@ +.loading-spinner { + margin: auto; + width: 100px; +} diff --git a/app/assets/stylesheets/components/_space-misc.scss b/app/assets/stylesheets/components/_space-misc.scss index 61c0bf57b8b..df019512724 100644 --- a/app/assets/stylesheets/components/_space-misc.scss +++ b/app/assets/stylesheets/components/_space-misc.scss @@ -8,12 +8,15 @@ .pb-tiny { padding-bottom: $space-tiny; } .pt-tiny { padding-top: $space-tiny; } .px-tiny { padding-left: $space-tiny; padding-right: $space-tiny; } +.py-tiny { padding-bottom: $space-tiny; padding-top: $space-tiny; } .mb-12p { margin-bottom: 12px; } .mt-12p { margin-top: 12px; } .px-12p { padding-left: 12px; padding-right: 12px; } .py-12p { padding-bottom: 12px; padding-top: 12px; } +.pl-24p { padding-left: 24px; } + .mb-40p { margin-bottom: 40px; } .mtn1 { margin-top: -$space-1; } diff --git a/app/assets/stylesheets/components/_typography.scss b/app/assets/stylesheets/components/_typography.scss index 2c376505781..f1397b4070d 100644 --- a/app/assets/stylesheets/components/_typography.scss +++ b/app/assets/stylesheets/components/_typography.scss @@ -14,6 +14,7 @@ body { -webkit-font-smoothing: antialiased; } .ls-5 { letter-spacing: 5px; } .fs-12p { font-size: 12px; } +.fs-13p { font-size: 13px; } .fs-20p { font-size: 20px; } .caps { diff --git a/app/assets/stylesheets/components/all.scss b/app/assets/stylesheets/components/all.scss index 8937fd95861..71f8a80ed93 100644 --- a/app/assets/stylesheets/components/all.scss +++ b/app/assets/stylesheets/components/all.scss @@ -10,6 +10,7 @@ @import 'form'; @import 'icon'; @import 'list'; +@import 'loading'; @import 'modal'; @import 'nav'; @import 'password'; @@ -25,3 +26,4 @@ @import 'space-addon'; @import 'space-misc'; @import 'typography'; +@import 'i18n-dropdown'; diff --git a/app/assets/stylesheets/variables/_web.scss b/app/assets/stylesheets/variables/_web.scss index 7a632869bf9..6a7c2abae08 100644 --- a/app/assets/stylesheets/variables/_web.scss +++ b/app/assets/stylesheets/variables/_web.scss @@ -99,7 +99,7 @@ $breakpoint-xl: '(min-width: 96em)' !default; $breakpoint-sm-md: '(min-width: 40em) and (max-width: 52em)' !default; $breakpoint-md-lg: '(min-width: 52em) and (max-width: 64em)' !default; -$container-width: 780px !default; +$container-width: 940px !default; $container-skinny-width: 620px !default; $container-xskinny-width: 416px !default; $container-xxskinny-width: 296px !default; diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 76da4d0180a..c623af396b6 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,6 +1,7 @@ class ApplicationController < ActionController::Base include UserSessionContext include VerifyProfileConcern + include LocaleHelper FLASH_KEYS = %w[alert error notice success warning].freeze @@ -53,6 +54,10 @@ def decorated_session ).call end + def default_url_options + { locale: locale_url_param } + end + private def disable_caching @@ -136,9 +141,7 @@ def skip_session_expiration end def set_locale - I18n.locale = - http_accept_language.compatible_language_from(I18n.available_locales) || - I18n.default_locale + I18n.locale = LocaleChooser.new(params[:locale], request).locale end def sp_session diff --git a/app/controllers/concerns/account_recovery_concern.rb b/app/controllers/concerns/account_reactivation_concern.rb similarity index 90% rename from app/controllers/concerns/account_recovery_concern.rb rename to app/controllers/concerns/account_reactivation_concern.rb index 46a36b11026..4c0140fcbcf 100644 --- a/app/controllers/concerns/account_recovery_concern.rb +++ b/app/controllers/concerns/account_reactivation_concern.rb @@ -1,4 +1,4 @@ -module AccountRecoveryConcern +module AccountReactivationConcern extend ActiveSupport::Concern def confirm_password_reset_profile diff --git a/app/controllers/concerns/fully_authenticatable.rb b/app/controllers/concerns/fully_authenticatable.rb index f5e7eb0665c..a0326a0ff11 100644 --- a/app/controllers/concerns/fully_authenticatable.rb +++ b/app/controllers/concerns/fully_authenticatable.rb @@ -9,7 +9,6 @@ def confirm_two_factor_authenticated(id = nil) def delete_branded_experience ServiceProviderRequest.from_uuid(sp_session[:request_id]).delete - session.delete(:sp) end def request_id diff --git a/app/controllers/concerns/idv_failure_concern.rb b/app/controllers/concerns/idv_failure_concern.rb index 689a3e61205..9956107d8f3 100644 --- a/app/controllers/concerns/idv_failure_concern.rb +++ b/app/controllers/concerns/idv_failure_concern.rb @@ -5,15 +5,28 @@ def render_failure if step_attempts_exceeded? @view_model = view_model(error: 'fail') flash_message(type: :error) - elsif step.form_valid_but_vendor_validation_failed? - @view_model = view_model(error: 'warning') + elsif form_valid_but_vendor_validation_failed? + @view_model = view_model(error: 'warning', timed_out: step.vendor_validation_timed_out?) flash_message(type: :warning) else @view_model = view_model end end + def form_valid_but_vendor_validation_failed? + idv_form.valid? && !step.vendor_validation_passed? + end + def flash_message(type:) flash.now[type.to_sym] = @view_model.flash_message end + + def view_model(error: nil, timed_out: nil) + view_model_class.new( + error: error, + remaining_attempts: remaining_step_attempts, + idv_form: idv_form, + timed_out: timed_out + ) + end end diff --git a/app/controllers/concerns/idv_session.rb b/app/controllers/concerns/idv_session.rb index 271de8555f2..62241be17bb 100644 --- a/app/controllers/concerns/idv_session.rb +++ b/app/controllers/concerns/idv_session.rb @@ -2,6 +2,7 @@ module IdvSession extend ActiveSupport::Concern def confirm_idv_session_started + return if current_user.decorate.needs_profile_usps_verification? redirect_to verify_session_url if idv_session.params.blank? end @@ -39,4 +40,32 @@ def idv_vendor def idv_attempter @_idv_attempter ||= Idv::Attempter.new(current_user) end + + def vendor_validator_result + return timed_out_vendor_error if vendor_result_timed_out? + + VendorValidatorResultStorage.new.load(idv_session.async_result_id) + end + + def vendor_result_timed_out? + started_at = idv_session.async_result_started_at + return false if started_at.blank? + + expiration = started_at + Figaro.env.async_job_refresh_max_wait_seconds.to_i + Time.zone.now.to_i >= expiration + end + + def timed_out_vendor_error + Idv::VendorResult.new( + success: false, + errors: { timed_out: ['Timed out waiting for vendor response'] }, + timed_out: true + ) + end + + def refresh_if_not_ready + return if vendor_validator_result.present? + + render 'shared/refresh' + end end diff --git a/app/controllers/concerns/phone_confirmation.rb b/app/controllers/concerns/phone_confirmation.rb index dbc99fc1880..efb5507ed35 100644 --- a/app/controllers/concerns/phone_confirmation.rb +++ b/app/controllers/concerns/phone_confirmation.rb @@ -4,7 +4,17 @@ def prompt_to_confirm_phone(phone:, context: 'confirmation') user_session[:context] = context redirect_to otp_send_path( - otp_delivery_selection_form: { otp_delivery_preference: current_user.otp_delivery_preference } + otp_delivery_selection_form: { otp_delivery_preference: otp_delivery_method(phone) } ) end + + private + + def otp_delivery_method(phone) + if PhoneNumberCapabilities.new(phone).sms_only? + :sms + else + current_user.otp_delivery_preference + end + end end diff --git a/app/controllers/concerns/two_factor_authenticatable.rb b/app/controllers/concerns/two_factor_authenticatable.rb index e728c786c23..02f005fd2a3 100644 --- a/app/controllers/concerns/two_factor_authenticatable.rb +++ b/app/controllers/concerns/two_factor_authenticatable.rb @@ -163,18 +163,23 @@ def update_phone_attributes end def update_idv_state - now = Time.zone.now if idv_context? - Idv::Session.new( - user_session: user_session, - current_user: current_user, - issuer: sp_session[:issuer] - ).params['phone_confirmed_at'] = now + confirm_idv_session_phone elsif profile_context? Idv::ProfileActivator.new(user: current_user).call end end + def confirm_idv_session_phone + idv_session = Idv::Session.new( + user_session: user_session, + current_user: current_user, + issuer: sp_session[:issuer] + ) + idv_session.user_phone_confirmation = true + idv_session.params['phone_confirmed_at'] = Time.zone.now + end + def reset_otp_session_data user_session.delete(:unconfirmed_phone) user_session[:context] = 'authentication' @@ -232,6 +237,7 @@ def phone_view_data phone_number: display_phone_to_deliver_to, code_value: direct_otp_code, otp_delivery_preference: two_factor_authentication_method, + voice_otp_delivery_unsupported: voice_otp_delivery_unsupported?, reenter_phone_number_path: reenter_phone_number_path, unconfirmed_phone: unconfirmed_phone?, totp_enabled: current_user.totp_enabled?, @@ -260,6 +266,15 @@ def display_phone_to_deliver_to end end + def voice_otp_delivery_unsupported? + phone_number = if authentication_context? + current_user.phone + else + user_session[:unconfirmed_phone] + end + PhoneNumberCapabilities.new(phone_number).sms_only? + end + def decorated_user current_user.decorate end diff --git a/app/controllers/reactivate_account_controller.rb b/app/controllers/reactivate_account_controller.rb index a9eff6cc5e4..dbb0eccba49 100644 --- a/app/controllers/reactivate_account_controller.rb +++ b/app/controllers/reactivate_account_controller.rb @@ -1,5 +1,5 @@ class ReactivateAccountController < ApplicationController - include AccountRecoveryConcern + include AccountReactivationConcern before_action :confirm_two_factor_authenticated before_action :confirm_password_reset_profile diff --git a/app/controllers/two_factor_authentication/totp_verification_controller.rb b/app/controllers/two_factor_authentication/totp_verification_controller.rb index d74ac3d3a69..5006c7ea17e 100644 --- a/app/controllers/two_factor_authentication/totp_verification_controller.rb +++ b/app/controllers/two_factor_authentication/totp_verification_controller.rb @@ -3,6 +3,7 @@ class TotpVerificationController < ApplicationController include TwoFactorAuthenticatable skip_before_action :handle_two_factor_authentication + before_action :confirm_totp_enabled def show @presenter = presenter_for_two_factor_authentication_method @@ -19,5 +20,13 @@ def create handle_invalid_otp end end + + private + + def confirm_totp_enabled + return if current_user.totp_enabled? + + redirect_to user_two_factor_authentication_path + end end end diff --git a/app/controllers/users/phones_controller.rb b/app/controllers/users/phones_controller.rb index 3fbeaadf54f..7d8c2c05be3 100644 --- a/app/controllers/users/phones_controller.rb +++ b/app/controllers/users/phones_controller.rb @@ -11,7 +11,7 @@ def edit def update @update_user_phone_form = UpdateUserPhoneForm.new(current_user) - if @update_user_phone_form.submit(user_params) + if @update_user_phone_form.submit(user_params).success? process_updates bypass_sign_in current_user else @@ -22,7 +22,7 @@ def update private def user_params - params.require(:update_user_phone_form).permit(:phone) + params.require(:update_user_phone_form).permit(:phone, :international_code) end def process_updates diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb index ae84123fdb3..078c3739a06 100644 --- a/app/controllers/users/sessions_controller.rb +++ b/app/controllers/users/sessions_controller.rb @@ -44,7 +44,7 @@ def timeout def check_user_needs_redirect if user_fully_authenticated? - redirect_to after_sign_in_path_for(current_user) + redirect_to signed_in_path elsif current_user sign_out end diff --git a/app/controllers/users/two_factor_authentication_controller.rb b/app/controllers/users/two_factor_authentication_controller.rb index 90c9e42a9ea..f83ae1c6bfc 100644 --- a/app/controllers/users/two_factor_authentication_controller.rb +++ b/app/controllers/users/two_factor_authentication_controller.rb @@ -14,7 +14,7 @@ def show end def send_code - @otp_delivery_selection_form = OtpDeliverySelectionForm.new(current_user) + @otp_delivery_selection_form = OtpDeliverySelectionForm.new(current_user, phone_to_deliver_to) result = @otp_delivery_selection_form.submit(delivery_params) diff --git a/app/controllers/users/two_factor_authentication_setup_controller.rb b/app/controllers/users/two_factor_authentication_setup_controller.rb index 7402f09ac09..5d21cf83494 100644 --- a/app/controllers/users/two_factor_authentication_setup_controller.rb +++ b/app/controllers/users/two_factor_authentication_setup_controller.rb @@ -9,6 +9,7 @@ class TwoFactorAuthenticationSetupController < ApplicationController def index @two_factor_setup_form = TwoFactorSetupForm.new(current_user) + @unsupported_area_codes = PhoneNumberCapabilities::VOICE_UNSUPPORTED_US_AREA_CODES analytics.track_event(Analytics::USER_REGISTRATION_PHONE_SETUP_VISIT) end diff --git a/app/controllers/users/verify_password_controller.rb b/app/controllers/users/verify_password_controller.rb index 67bea6d230f..2dc268cb5e4 100644 --- a/app/controllers/users/verify_password_controller.rb +++ b/app/controllers/users/verify_password_controller.rb @@ -1,6 +1,6 @@ module Users class VerifyPasswordController < ApplicationController - include AccountRecoveryConcern + include AccountReactivationConcern before_action :confirm_two_factor_authenticated before_action :confirm_password_reset_profile diff --git a/app/controllers/users/verify_personal_key_controller.rb b/app/controllers/users/verify_personal_key_controller.rb index 6316fc9441b..0e7fbe39643 100644 --- a/app/controllers/users/verify_personal_key_controller.rb +++ b/app/controllers/users/verify_personal_key_controller.rb @@ -1,10 +1,10 @@ module Users class VerifyPersonalKeyController < ApplicationController - include AccountRecoveryConcern + include AccountReactivationConcern before_action :confirm_two_factor_authenticated before_action :confirm_password_reset_profile - before_action :init_account_recovery, only: [:new] + before_action :init_account_reactivation, only: [:new] def new @personal_key_form = VerifyPersonalKeyForm.new( @@ -25,10 +25,10 @@ def create private - def init_account_recovery + def init_account_reactivation return if reactivate_account_session.started? - flash.now[:notice] = t('notices.account_recovery') + flash.now[:notice] = t('notices.account_reactivation') reactivate_account_session.start end diff --git a/app/controllers/verify/confirmations_controller.rb b/app/controllers/verify/confirmations_controller.rb index 685a8f2429b..a359a255956 100644 --- a/app/controllers/verify/confirmations_controller.rb +++ b/app/controllers/verify/confirmations_controller.rb @@ -33,7 +33,7 @@ def confirm_profile_has_been_created def track_final_idv_event result = { success: true, - new_phone_added: idv_session.params['phone_confirmed_at'].present?, + new_phone_added: idv_session.params['phone'] != current_user.phone, } analytics.track_event(Analytics::IDV_FINAL, result) end diff --git a/app/controllers/verify/finance_controller.rb b/app/controllers/verify/finance_controller.rb index e41a5af7c4f..2f4110552e9 100644 --- a/app/controllers/verify/finance_controller.rb +++ b/app/controllers/verify/finance_controller.rb @@ -5,6 +5,7 @@ class FinanceController < ApplicationController before_action :confirm_step_needed before_action :confirm_step_allowed + before_action :refresh_if_not_ready, only: [:show] def new @view_model = view_model @@ -12,8 +13,21 @@ def new end def create + result = idv_form.submit(step_params) + analytics.track_event(Analytics::IDV_FINANCE_CONFIRMATION_FORM, result.to_h) + + if result.success? + submit_idv_job + redirect_to verify_finance_result_path + else + @view_model = view_model + render_form + end + end + + def show result = step.submit - analytics.track_event(Analytics::IDV_FINANCE_CONFIRMATION, result.to_h) + analytics.track_event(Analytics::IDV_FINANCE_CONFIRMATION_VENDOR, result.to_h) increment_step_attempts if result.success? @@ -26,6 +40,14 @@ def create private + def submit_idv_job + SubmitIdvJob.new( + vendor_validator_class: Idv::FinancialsValidator, + idv_session: idv_session, + vendor_params: vendor_params + ).call + end + def step_name :financials end @@ -34,16 +56,12 @@ def confirm_step_needed redirect_to verify_address_path if idv_session.financials_confirmation == true end - def view_model(error: nil) - Verify::FinancialsNew.new( - error: error, - remaining_attempts: remaining_step_attempts, - idv_form: idv_finance_form - ) + def view_model_class + Verify::FinancialsNew end - def idv_finance_form - @_idv_finance_form ||= Idv::FinanceForm.new(idv_session.params) + def idv_form + @_idv_form ||= Idv::FinanceForm.new(idv_session.params) end def handle_success @@ -53,9 +71,9 @@ def handle_success def step @_step ||= Idv::FinancialsStep.new( - idv_form: idv_finance_form, + idv_form_params: idv_form.idv_params, idv_session: idv_session, - params: step_params + vendor_validator_result: vendor_validator_result ) end @@ -64,11 +82,16 @@ def step_params end def render_form - if step_params[:finance_type] == 'ccn' + if idv_form.idv_params[:ccn].present? render :new else render 'verify/finance_other/new' end end + + def vendor_params + finance_type = idv_form.finance_type + { finance_type => idv_form.idv_params[finance_type] } + end end end diff --git a/app/controllers/verify/phone_controller.rb b/app/controllers/verify/phone_controller.rb index 0a941349b2f..888b08ac2b9 100644 --- a/app/controllers/verify/phone_controller.rb +++ b/app/controllers/verify/phone_controller.rb @@ -5,6 +5,7 @@ class PhoneController < ApplicationController before_action :confirm_step_needed before_action :confirm_step_allowed + before_action :refresh_if_not_ready, only: [:show] def new @view_model = view_model @@ -12,8 +13,21 @@ def new end def create + result = idv_form.submit(step_params) + analytics.track_event(Analytics::IDV_PHONE_CONFIRMATION_FORM, result.to_h) + + if result.success? + submit_idv_job + redirect_to verify_phone_result_path + else + @view_model = view_model + render :new + end + end + + def show result = step.submit - analytics.track_event(Analytics::IDV_PHONE_CONFIRMATION, result.to_h) + analytics.track_event(Analytics::IDV_PHONE_CONFIRMATION_VENDOR, result.to_h) increment_step_attempts if result.success? @@ -26,24 +40,28 @@ def create private + def submit_idv_job + SubmitIdvJob.new( + vendor_validator_class: Idv::PhoneValidator, + idv_session: idv_session, + vendor_params: idv_session.params[:phone] + ).call + end + def step_name :phone end def step @_step ||= Idv::PhoneStep.new( - idv_form: idv_phone_form, idv_session: idv_session, - params: step_params + idv_form_params: idv_form.idv_params, + vendor_validator_result: vendor_validator_result ) end - def view_model(error: nil) - Verify::PhoneNew.new( - error: error, - remaining_attempts: remaining_step_attempts, - idv_form: idv_phone_form - ) + def view_model_class + Verify::PhoneNew end def step_params @@ -51,11 +69,11 @@ def step_params end def confirm_step_needed - redirect_to verify_review_path if idv_session.phone_confirmation == true + redirect_to verify_review_path if idv_session.vendor_phone_confirmation == true end - def idv_phone_form - @_idv_phone_form ||= Idv::PhoneForm.new(idv_session.params, current_user) + def idv_form + @_idv_form ||= Idv::PhoneForm.new(idv_session.params, current_user) end end end diff --git a/app/controllers/verify/review_controller.rb b/app/controllers/verify/review_controller.rb index 7eec52cf1a7..b55e8584093 100644 --- a/app/controllers/verify/review_controller.rb +++ b/app/controllers/verify/review_controller.rb @@ -81,7 +81,11 @@ def idv_params end def phone_confirmation_required? - idv_params[:phone] != current_user.phone && + normalized_phone = idv_params[:phone] + return false if normalized_phone.blank? + + formatted_phone = PhoneFormatter.new.format(normalized_phone) + formatted_phone != current_user.phone && idv_session.address_verification_mechanism == 'phone' end diff --git a/app/controllers/verify/sessions_controller.rb b/app/controllers/verify/sessions_controller.rb index 6d944c73fea..21031c957db 100644 --- a/app/controllers/verify/sessions_controller.rb +++ b/app/controllers/verify/sessions_controller.rb @@ -7,6 +7,8 @@ class SessionsController < ApplicationController before_action :confirm_idv_attempts_allowed before_action :confirm_idv_needed before_action :confirm_step_needed, except: [:destroy] + before_action :initialize_idv_session, only: [:create] + before_action :refresh_if_not_ready, only: [:show] delegate :attempts_exceeded?, to: :step, prefix: true @@ -17,8 +19,20 @@ def new end def create + result = idv_form.submit(profile_params) + analytics.track_event(Analytics::IDV_BASIC_INFO_SUBMITTED_FORM, result.to_h) + + if result.success? + submit_idv_job + redirect_to verify_session_result_path + else + process_failure + end + end + + def show result = step.submit - analytics.track_event(Analytics::IDV_BASIC_INFO_SUBMITTED, result.to_h) + analytics.track_event(Analytics::IDV_BASIC_INFO_SUBMITTED_VENDOR, result.to_h) if result.success? process_success @@ -35,6 +49,14 @@ def destroy private + def submit_idv_job + SubmitIdvJob.new( + vendor_validator_class: Idv::ProfileValidator, + idv_session: idv_session, + vendor_params: idv_session.vendor_params + ).call + end + def step_name :sessions end @@ -45,9 +67,9 @@ def confirm_step_needed def step @_step ||= Idv::ProfileStep.new( - idv_form: idv_profile_form, + idv_form_params: idv_session.params, idv_session: idv_session, - params: profile_params + vendor_validator_result: vendor_validator_result ) end @@ -68,7 +90,7 @@ def process_success end def process_failure - if step.duplicate_ssn? + if idv_form.duplicate_ssn? flash[:error] = t('idv.errors.duplicate_ssn') redirect_to verify_session_dupe_path else @@ -77,20 +99,21 @@ def process_failure end end - def view_model(error: nil) - Verify::SessionsNew.new( - error: error, - remaining_attempts: remaining_idv_attempts, - idv_form: idv_profile_form - ) + def view_model_class + Verify::SessionsNew end - def remaining_idv_attempts + def remaining_step_attempts Idv::Attempter.idv_max_attempts - current_user.idv_attempts end - def idv_profile_form - @_idv_profile_form ||= Idv::ProfileForm.new((idv_session.params || {}), current_user) + def idv_form + @_idv_form ||= Idv::ProfileForm.new((idv_session.params || {}), current_user) + end + + def initialize_idv_session + idv_session.params.merge!(profile_params) + idv_session.applicant = idv_session.vendor_params end def profile_params diff --git a/app/controllers/verify/usps_controller.rb b/app/controllers/verify/usps_controller.rb index 5a72c9b68b5..bc850e6c921 100644 --- a/app/controllers/verify/usps_controller.rb +++ b/app/controllers/verify/usps_controller.rb @@ -5,10 +5,7 @@ class UspsController < ApplicationController before_action :confirm_mail_not_spammed def index - @applicant = idv_session.normalized_applicant_params - decorated_usps = UspsDecorator.new(idv_session) - @title = decorated_usps.title - @button = decorated_usps.button + @decorated_usps = UspsDecorator.new(usps_mail_service) end def create diff --git a/app/controllers/verify_controller.rb b/app/controllers/verify_controller.rb index 1f62949184a..b4ac0e88992 100644 --- a/app/controllers/verify_controller.rb +++ b/app/controllers/verify_controller.rb @@ -1,6 +1,6 @@ class VerifyController < ApplicationController include IdvSession - include AccountRecoveryConcern + include AccountReactivationConcern before_action :confirm_two_factor_authenticated before_action :confirm_idv_needed, only: %i[cancel fail] diff --git a/app/decorators/usps_decorator.rb b/app/decorators/usps_decorator.rb index 6777bc95cc5..31e3845a556 100644 --- a/app/decorators/usps_decorator.rb +++ b/app/decorators/usps_decorator.rb @@ -1,8 +1,8 @@ class UspsDecorator - attr_reader :idv_session + attr_reader :usps_mail_service - def initialize(idv_session) - @idv_session = idv_session + def initialize(usps_mail_service) + @usps_mail_service = usps_mail_service end def title @@ -16,6 +16,6 @@ def button private def letter_already_sent? - @idv_session.address_verification_mechanism == 'usps' + @usps_mail_service.any_mail_sent? end end diff --git a/app/forms/idv/finance_form.rb b/app/forms/idv/finance_form.rb index 8746f69e46d..9becfcec15d 100644 --- a/app/forms/idv/finance_form.rb +++ b/app/forms/idv/finance_form.rb @@ -37,6 +37,7 @@ class FinanceForm def initialize(idv_params) @idv_params = idv_params + @params = nil finance_type = FINANCE_TYPES.find { |param| idv_params.key? param } update_finance_values(idv_params.merge(finance_type: finance_type)) end @@ -44,11 +45,14 @@ def initialize(idv_params) def submit(params) @params = params finance_value = update_finance_values(params) - return false unless valid? - clear_idv_params_finance - idv_params[finance_type] = finance_value - true + success = valid? + if success + clear_idv_params_finance + idv_params[finance_type] = finance_value + end + + FormResponse.new(success: success, errors: errors.messages) end def self.finance_other_type_choices @@ -71,6 +75,10 @@ def self.finance_other_type_inputs attr_writer :finance_type, *FINANCE_TYPES + def params + @params.presence || idv_params + end + def update_finance_values(params) type = params[:finance_type] return false unless valid_finance_type?(type) diff --git a/app/forms/idv/phone_form.rb b/app/forms/idv/phone_form.rb index c6161e7947f..f59e0b4a6bf 100644 --- a/app/forms/idv/phone_form.rb +++ b/app/forms/idv/phone_form.rb @@ -4,27 +4,28 @@ class PhoneForm include FormPhoneValidator attr_reader :idv_params, :user, :phone + attr_accessor :international_code def initialize(idv_params, user) @idv_params = idv_params @user = user - self.phone = idv_params[:phone] || user.phone + self.phone = (idv_params[:phone] || user.phone).phony_formatted( + format: :international, normalize: :US, spaces: ' ' + ) + self.international_code = PhoneFormatter::DEFAULT_COUNTRY end def submit(params) submitted_phone = params[:phone] - formatted_phone = submitted_phone.phony_formatted( - format: :international, normalize: :US, spaces: ' ' - ) + formatted_phone = PhoneFormatter.new.format(submitted_phone, country_code: international_code) self.phone = formatted_phone - return false unless valid? - - update_idv_params(formatted_phone) + success = valid? + update_idv_params(formatted_phone) if success - true + FormResponse.new(success: success, errors: errors.messages) end private @@ -32,7 +33,8 @@ def submit(params) attr_writer :phone def update_idv_params(phone) - idv_params[:phone] = phone + normalized_phone = phone.gsub(/\D/, '')[1..-1] + idv_params[:phone] = normalized_phone return if phone != user.phone diff --git a/app/forms/idv/profile_form.rb b/app/forms/idv/profile_form.rb index 720b26f6b1b..7d2912f44b5 100644 --- a/app/forms/idv/profile_form.rb +++ b/app/forms/idv/profile_form.rb @@ -40,6 +40,13 @@ def pii_attributes def submit(params) initialize_params(params) profile.ssn_signature = ssn_signature + + FormResponse.new(success: valid?, errors: errors.messages) + end + + def duplicate_ssn? + return true if any_matching_ssn_signatures?(ssn_signature) + return true if ssn_is_duplicate_with_old_key? end private @@ -59,12 +66,7 @@ def ssn_signature(key = Pii::Fingerprinter.current_key) end def ssn_is_unique - errors.add :ssn, I18n.t('idv.errors.duplicate_ssn') if ssn_is_duplicate? - end - - def ssn_is_duplicate? - return true if any_matching_ssn_signatures?(ssn_signature) - return true if ssn_is_duplicate_with_old_key? + errors.add :ssn, I18n.t('idv.errors.duplicate_ssn') if duplicate_ssn? end def ssn_is_duplicate_with_old_key? diff --git a/app/forms/otp_delivery_selection_form.rb b/app/forms/otp_delivery_selection_form.rb index b62e5a0d6d9..4ed925264a5 100644 --- a/app/forms/otp_delivery_selection_form.rb +++ b/app/forms/otp_delivery_selection_form.rb @@ -5,8 +5,9 @@ class OtpDeliverySelectionForm validates :otp_delivery_preference, inclusion: { in: %w[sms voice] } - def initialize(user) + def initialize(user, phone_to_deliver_to) @user = user + @phone_to_deliver_to = phone_to_deliver_to end def submit(params) @@ -27,7 +28,7 @@ def submit(params) attr_writer :otp_delivery_preference attr_accessor :resend - attr_reader :success, :user + attr_reader :success, :user, :phone_to_deliver_to def otp_delivery_preference_changed? otp_delivery_preference != user.otp_delivery_preference @@ -37,6 +38,12 @@ def extra_analytics_attributes { otp_delivery_preference: otp_delivery_preference, resend: resend, + country_code: parsed_phone.country_code, + area_code: parsed_phone.area_code, } end + + def parsed_phone + @_parsed_phone ||= Phonelib.parse(phone_to_deliver_to) + end end diff --git a/app/forms/two_factor_setup_form.rb b/app/forms/two_factor_setup_form.rb index 1d648c332b2..bc8a123dc8e 100644 --- a/app/forms/two_factor_setup_form.rb +++ b/app/forms/two_factor_setup_form.rb @@ -1,17 +1,16 @@ class TwoFactorSetupForm include ActiveModel::Model include FormPhoneValidator + include OtpDeliveryPreferenceValidator - attr_accessor :phone + attr_accessor :phone, :international_code def initialize(user) @user = user end def submit(params) - self.phone = params[:phone].phony_formatted( - format: :international, normalize: :US, spaces: ' ' - ) + process_phone_number_params(params) self.otp_delivery_preference = params[:otp_delivery_preference] @success = valid? @@ -21,6 +20,11 @@ def submit(params) FormResponse.new(success: success, errors: errors.messages, extra: extra_analytics_attributes) end + def process_phone_number_params(params) + self.international_code = params[:international_code] + self.phone = PhoneFormatter.new.format(params[:phone], country_code: international_code) + end + private attr_reader :success, :user diff --git a/app/forms/update_user_phone_form.rb b/app/forms/update_user_phone_form.rb index 538cf17b310..16f06558ead 100644 --- a/app/forms/update_user_phone_form.rb +++ b/app/forms/update_user_phone_form.rb @@ -2,7 +2,7 @@ class UpdateUserPhoneForm include ActiveModel::Model include FormPhoneValidator - attr_accessor :phone + attr_accessor :phone, :international_code attr_reader :user def persisted? @@ -12,12 +12,16 @@ def persisted? def initialize(user) @user = user self.phone = @user.phone + self.international_code = Phonelib.parse(phone).country || PhoneFormatter::DEFAULT_COUNTRY end def submit(params) - check_phone_change(params) + self.phone = params[:phone] + self.international_code = params[:international_code] - valid? + check_phone_change + + FormResponse.new(success: valid?, errors: errors.messages) end def phone_changed? @@ -28,10 +32,8 @@ def phone_changed? attr_reader :phone_changed - def check_phone_change(params) - formatted_phone = params[:phone].phony_formatted( - format: :international, normalize: :US, spaces: ' ' - ) + def check_phone_change + formatted_phone = PhoneFormatter.new.format(phone, country_code: international_code) return unless formatted_phone != @user.phone diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb index 7db2fffb178..73b8c819fed 100644 --- a/app/helpers/form_helper.rb +++ b/app/helpers/form_helper.rb @@ -66,4 +66,28 @@ def us_states_territories ] end # rubocop:enable MethodLength, WordArray + + def international_phone_codes + PhoneNumberCapabilities::INTERNATIONAL_CODES.map do |key, value| + [ + international_phone_code_label(value), + key, + { data: international_phone_codes_data(value) }, + ] + end + end + + private + + def international_phone_code_label(code_data) + "#{code_data['name']} +#{code_data['country_code']}" + end + + def international_phone_codes_data(code_data) + { + sms_only: code_data['sms_only'], + country_code: code_data['country_code'], + country_name: code_data['name'], + } + end end diff --git a/app/helpers/locale_helper.rb b/app/helpers/locale_helper.rb new file mode 100644 index 00000000000..ff18d64724e --- /dev/null +++ b/app/helpers/locale_helper.rb @@ -0,0 +1,6 @@ +module LocaleHelper + def locale_url_param + active_locale = I18n.locale + active_locale == I18n.default_locale ? nil : active_locale + end +end diff --git a/app/jobs/sms_otp_sender_job.rb b/app/jobs/sms_otp_sender_job.rb index af203aafec0..ccfe06a0d04 100644 --- a/app/jobs/sms_otp_sender_job.rb +++ b/app/jobs/sms_otp_sender_job.rb @@ -15,7 +15,16 @@ def otp_valid?(otp_created_at) def send_otp(twilio_service, code, phone) twilio_service.send_sms( to: phone, - body: "#{code} is your #{APP_NAME} one-time security code." + body: I18n.t('jobs.sms_otp_sender_job.message', code: code, app: APP_NAME) ) + rescue Twilio::REST::RequestError => error + sanitize_phone_number(error.message) + raise + end + + def sanitize_phone_number(str) + return unless str =~ /is not a valid phone number/ + + str.gsub!(/\+[\d\(\)\- ]+/) { |match| match.gsub(/\d/, '#') } end end diff --git a/app/jobs/vendor_validator_job.rb b/app/jobs/vendor_validator_job.rb new file mode 100644 index 00000000000..cf0f13ac295 --- /dev/null +++ b/app/jobs/vendor_validator_job.rb @@ -0,0 +1,37 @@ +class VendorValidatorJob < ActiveJob::Base + queue_as :idv + + def perform(result_id:, vendor_validator_class:, vendor:, vendor_params:, applicant_json:, + vendor_session_id:) + vendor_validator = vendor_validator_class.constantize.new( + applicant: Proofer::Applicant.new(JSON.parse(applicant_json, symbolize_names: true)), + vendor: vendor.to_sym, + vendor_params: indifferent_access(vendor_params), + vendor_session_id: vendor_session_id + ) + + VendorValidatorResultStorage.new.store( + result_id: result_id, + result: extract_result(vendor_validator.result) + ) + end + + private + + def extract_result(result) + vendor_resp = result.vendor_resp + + Idv::VendorResult.new( + success: result.success?, + errors: result.errors, + reasons: vendor_resp.reasons, + normalized_applicant: vendor_resp.try(:normalized_applicant), + session_id: result.try(:session_id) + ) + end + + def indifferent_access(params) + return params if params.is_a?(String) + params.with_indifferent_access + end +end diff --git a/app/mailers/custom_devise_mailer.rb b/app/mailers/custom_devise_mailer.rb index efed6f6b003..ca9cfb44757 100644 --- a/app/mailers/custom_devise_mailer.rb +++ b/app/mailers/custom_devise_mailer.rb @@ -1,14 +1,21 @@ class CustomDeviseMailer < Devise::Mailer include Mailable + include LocaleHelper before_action :attach_images layout 'layouts/user_mailer' default from: email_with_name(Figaro.env.email_from, Figaro.env.email_from) + def reset_password_instructions(*) + @locale = locale_url_param + super + end + def confirmation_instructions(record, token, options = {}) presenter = ConfirmationEmailPresenter.new(record, view_context) @first_sentence = presenter.first_sentence @confirmation_period = presenter.confirmation_period @request_id = options[:request_id] + @locale = locale_url_param super end end diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index 6be2598e599..aff54835ba5 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -1,5 +1,6 @@ class UserMailer < ActionMailer::Base include Mailable + include LocaleHelper before_action :attach_images default from: email_with_name(Figaro.env.email_from, Figaro.env.email_from) @@ -8,8 +9,7 @@ def email_changed(old_email) end def signup_with_your_email(email) - @root_url = root_url - @new_user_password_url = new_user_password_url + @root_url = root_url(locale: locale_url_param) mail(to: email, subject: t('mailer.email_reuse_notice.subject')) end diff --git a/app/presenters/two_factor_auth_code/authenticator_delivery_presenter.rb b/app/presenters/two_factor_auth_code/authenticator_delivery_presenter.rb index fbf6a2736e3..f7505b5bd20 100644 --- a/app/presenters/two_factor_auth_code/authenticator_delivery_presenter.rb +++ b/app/presenters/two_factor_auth_code/authenticator_delivery_presenter.rb @@ -5,7 +5,7 @@ def header end def help_text - t("instructions.2fa.#{two_factor_authentication_method}.confirm_code_html", + t("instructions.mfa.#{two_factor_authentication_method}.confirm_code_html", email: content_tag(:strong, user_email), app: content_tag(:strong, APP_NAME), tooltip: view.tooltip(t('tooltips.authentication_app'))) diff --git a/app/presenters/two_factor_auth_code/phone_delivery_presenter.rb b/app/presenters/two_factor_auth_code/phone_delivery_presenter.rb index 411e52f9019..49c729bc0cf 100644 --- a/app/presenters/two_factor_auth_code/phone_delivery_presenter.rb +++ b/app/presenters/two_factor_auth_code/phone_delivery_presenter.rb @@ -5,7 +5,7 @@ def header end def help_text - t("instructions.2fa.#{otp_delivery_preference}.confirm_code_html", + t("instructions.mfa.#{otp_delivery_preference}.confirm_code_html", number: phone_number_tag, resend_code_link: resend_code_link) end @@ -34,6 +34,7 @@ def cancel_link :phone_number, :unconfirmed_phone, :otp_delivery_preference, + :voice_otp_delivery_unsupported, :confirmation_for_phone_change ) @@ -42,14 +43,26 @@ def phone_number_tag end def otp_fallback_options - safe_join([phone_fallback_link, auth_app_fallback_link]) + if totp_enabled + otp_fallback_options_with_totp + elsif !voice_otp_delivery_unsupported + safe_join([phone_fallback_link, '.']) + end + end + + def otp_fallback_options_with_totp + if voice_otp_delivery_unsupported + safe_join([auth_app_fallback_tag, '.']) + else + safe_join([phone_fallback_link, auth_app_fallback_link]) + end end def update_phone_link return unless unconfirmed_phone link = view.link_to(t('forms.two_factor.try_again'), reenter_phone_number_path) - t('instructions.2fa.wrong_number_html', link: link) + t('instructions.mfa.wrong_number_html', link: link) end def phone_fallback_link @@ -64,15 +77,9 @@ def phone_link_tag end def auth_app_fallback_link - return empty unless totp_enabled - t('links.phone_confirmation.auth_app_fallback_html', link: auth_app_fallback_tag) end - def empty - '.' - end - def auth_app_fallback_tag view.link_to( t('links.two_factor_authentication.app'), @@ -81,7 +88,7 @@ def auth_app_fallback_tag end def fallback_instructions - "instructions.2fa.#{otp_delivery_preference}.fallback_html" + "instructions.mfa.#{otp_delivery_preference}.fallback_html" end def fallback_method diff --git a/app/services/analytics.rb b/app/services/analytics.rb index 96811367afd..6d59735fcbe 100644 --- a/app/services/analytics.rb +++ b/app/services/analytics.rb @@ -38,14 +38,17 @@ def uuid EMAIL_CONFIRMATION = 'Email Confirmation'.freeze EMAIL_CONFIRMATION_RESEND = 'Email Confirmation requested due to invalid token'.freeze IDV_BASIC_INFO_VISIT = 'IdV: basic info visited'.freeze - IDV_BASIC_INFO_SUBMITTED = 'IdV: basic info submitted'.freeze + IDV_BASIC_INFO_SUBMITTED_FORM = 'IdV: basic info form submitted'.freeze + IDV_BASIC_INFO_SUBMITTED_VENDOR = 'IdV: basic info vendor submitted'.freeze IDV_MAX_ATTEMPTS_EXCEEDED = 'IdV: max attempts exceeded'.freeze IDV_FINAL = 'IdV: final resolution'.freeze IDV_FINANCE_CCN_VISIT = 'IdV: finance ccn visited'.freeze - IDV_FINANCE_CONFIRMATION = 'IdV: finance confirmation'.freeze + IDV_FINANCE_CONFIRMATION_FORM = 'IdV: finance confirmation form'.freeze + IDV_FINANCE_CONFIRMATION_VENDOR = 'IdV: finance confirmation vendor'.freeze IDV_FINANCE_OTHER_VISIT = 'IdV: finance other visited'.freeze IDV_INTRO_VISIT = 'IdV: intro visited'.freeze - IDV_PHONE_CONFIRMATION = 'IdV: phone confirmation'.freeze + IDV_PHONE_CONFIRMATION_FORM = 'IdV: phone confirmation form'.freeze + IDV_PHONE_CONFIRMATION_VENDOR = 'IdV: phone confirmation vendor'.freeze IDV_PHONE_RECORD_VISIT = 'IdV: phone of record visited'.freeze IDV_REVIEW_COMPLETE = 'IdV: review complete'.freeze IDV_REVIEW_VISIT = 'IdV: review info visited'.freeze diff --git a/app/services/idv/financials_step.rb b/app/services/idv/financials_step.rb index cffa4264dd0..0979372b0b4 100644 --- a/app/services/idv/financials_step.rb +++ b/app/services/idv/financials_step.rb @@ -4,7 +4,7 @@ def submit if complete? @success = true idv_session.financials_confirmation = true - idv_session.params = idv_form.idv_params + idv_session.params = idv_form_params else @success = false idv_session.financials_confirmation = false @@ -13,29 +13,12 @@ def submit FormResponse.new(success: success, errors: errors) end - def form_valid_but_vendor_validation_failed? - form_valid? && !vendor_validation_passed? - end - private attr_reader :success def complete? - form_valid? && vendor_validation_passed? - end - - def vendor_validator_class - Idv::FinancialsValidator - end - - def vendor_reasons - vendor_validator.reasons if form_valid? - end - - def vendor_params - finance_type = idv_form.finance_type - { finance_type => idv_form.idv_params[finance_type] } + vendor_validation_passed? end end end diff --git a/app/services/idv/financials_validator.rb b/app/services/idv/financials_validator.rb index d4c4746b0c2..187dcabd348 100644 --- a/app/services/idv/financials_validator.rb +++ b/app/services/idv/financials_validator.rb @@ -2,13 +2,9 @@ module Idv class FinancialsValidator < VendorValidator private - def session_id - idv_session.vendor_session_id - end - def try_submit try_agent_action do - idv_agent.submit_financials(vendor_params, session_id) + idv_agent.submit_financials(vendor_params, vendor_session_id) end end end diff --git a/app/services/idv/phone_step.rb b/app/services/idv/phone_step.rb index 252b6f86f5d..eb4954ff099 100644 --- a/app/services/idv/phone_step.rb +++ b/app/services/idv/phone_step.rb @@ -4,38 +4,23 @@ def submit if complete? update_idv_session else - idv_session.phone_confirmation = false + idv_session.vendor_phone_confirmation = false end FormResponse.new(success: complete?, errors: errors) end - def form_valid_but_vendor_validation_failed? - form_valid? && !vendor_validation_passed? - end - private def complete? - form_valid? && vendor_validation_passed? - end - - def vendor_validator_class - Idv::PhoneValidator - end - - def vendor_params - idv_form.phone - end - - def vendor_reasons - vendor_validator.reasons if form_valid? + vendor_validation_passed? end def update_idv_session - idv_session.phone_confirmation = true + idv_session.vendor_phone_confirmation = true idv_session.address_verification_mechanism = :phone - idv_session.params = idv_form.idv_params + idv_session.params = idv_form_params + idv_session.user_phone_confirmation = idv_form_params[:phone_confirmed_at].present? end end end diff --git a/app/services/idv/phone_validator.rb b/app/services/idv/phone_validator.rb index e6bb1d3188a..7e4b26cc009 100644 --- a/app/services/idv/phone_validator.rb +++ b/app/services/idv/phone_validator.rb @@ -2,13 +2,9 @@ module Idv class PhoneValidator < VendorValidator private - def session_id - idv_session.vendor_session_id - end - def try_submit try_agent_action do - idv_agent.submit_phone(vendor_params, session_id) + idv_agent.submit_phone(vendor_params, vendor_session_id) end end end diff --git a/app/services/idv/profile_step.rb b/app/services/idv/profile_step.rb index 3e92d2314e6..371e6d2a71a 100644 --- a/app/services/idv/profile_step.rb +++ b/app/services/idv/profile_step.rb @@ -1,12 +1,9 @@ module Idv class ProfileStep < Step def submit - initialize_idv_session - submit_idv_form - @success = complete? - increment_attempts_count if form_valid? + increment_attempts_count update_idv_session if success FormResponse.new(success: success, errors: errors, extra: extra_analytics_attributes) @@ -16,55 +13,26 @@ def attempts_exceeded? attempter.exceeded? end - def duplicate_ssn? - errors.key?(:ssn) && errors[:ssn].include?(I18n.t('idv.errors.duplicate_ssn')) - end - - def form_valid_but_vendor_validation_failed? - form_valid? && !vendor_validation_passed? - end - private attr_reader :success - def initialize_idv_session - idv_session.params.merge!(params) - idv_session.applicant = vendor_params - end - - def vendor_params - idv_session.vendor_params - end - - def submit_idv_form - idv_form.submit(params) - end - def complete? - !attempts_exceeded? && form_valid? && vendor_validation_passed? + !attempts_exceeded? && vendor_validation_passed? end def attempter - @_idv_attempter ||= Idv::Attempter.new(idv_form.user) + @_idv_attempter ||= Idv::Attempter.new(idv_session.current_user) end def increment_attempts_count attempter.increment end - def form_valid? - @_form_valid ||= idv_form.valid? - end - - def vendor_validator_class - Idv::ProfileValidator - end - def update_idv_session idv_session.profile_confirmation = true - idv_session.vendor_session_id = vendor_validator.session_id - idv_session.normalized_applicant_params = vendor_validator.normalized_applicant.to_hash + idv_session.vendor_session_id = vendor_validator_result.session_id + idv_session.normalized_applicant_params = vendor_validator_result.normalized_applicant.to_hash idv_session.resolution_successful = true end @@ -76,7 +44,7 @@ def extra_analytics_attributes end def vendor_reasons - vendor_validator.reasons if form_valid? + vendor_validator_result.reasons end end end diff --git a/app/services/idv/profile_validator.rb b/app/services/idv/profile_validator.rb index eb1cd5ce051..f3c72a064e3 100644 --- a/app/services/idv/profile_validator.rb +++ b/app/services/idv/profile_validator.rb @@ -1,15 +1,9 @@ module Idv class ProfileValidator < VendorValidator - delegate :session_id, to: :result - def result @_result ||= try_start end - def normalized_applicant - result.vendor_resp.normalized_applicant - end - private def try_start diff --git a/app/services/idv/session.rb b/app/services/idv/session.rb index 188ef878467..269517ffba9 100644 --- a/app/services/idv/session.rb +++ b/app/services/idv/session.rb @@ -1,12 +1,15 @@ module Idv class Session VALID_SESSION_ATTRIBUTES = %i[ + async_result_id + async_result_started_at address_verification_mechanism applicant financials_confirmation normalized_applicant_params params - phone_confirmation + vendor_phone_confirmation + user_phone_confirmation pii profile_confirmation profile_id @@ -17,11 +20,13 @@ class Session vendor_session_id ].freeze + attr_reader :current_user + def initialize(user_session:, current_user:, issuer:) @user_session = user_session @current_user = current_user @issuer = issuer - @user_session[:idv] ||= new_idv_session + set_idv_session end def method_missing(method_sym, *arguments, &block) @@ -67,8 +72,12 @@ def clear user_session.delete(:idv) end + def phone_confirmed? + vendor_phone_confirmation == true && user_phone_confirmation == true + end + def complete_session - complete_profile if phone_confirmation == true + complete_profile if phone_confirmed? create_usps_entry if address_verification_mechanism == 'usps' end @@ -91,12 +100,17 @@ def alive? end def address_mechanism_chosen? - phone_confirmation == true || address_verification_mechanism == 'usps' + vendor_phone_confirmation == true || address_verification_mechanism == 'usps' end private - attr_accessor :user_session, :current_user, :issuer + attr_accessor :user_session, :issuer + + def set_idv_session + return if session.present? + user_session[:idv] = new_idv_session + end def new_idv_session { params: {}, step_attempts: { financials: 0, phone: 0 } } @@ -108,7 +122,7 @@ def move_pii_to_user_session end def session - user_session[:idv] + user_session.fetch(:idv, {}) end def applicant_params @@ -123,7 +137,7 @@ def profile_maker @_profile_maker ||= Idv::ProfileMaker.new( applicant: Proofer::Applicant.new(applicant_params), normalized_applicant: Proofer::Applicant.new(normalized_applicant_params), - phone_confirmed: phone_confirmation || false, + phone_confirmed: vendor_phone_confirmation || false, user: current_user, vendor: vendor ) diff --git a/app/services/idv/step.rb b/app/services/idv/step.rb index 72e93cd0924..06125e4f933 100644 --- a/app/services/idv/step.rb +++ b/app/services/idv/step.rb @@ -1,51 +1,31 @@ # abstract base class for Idv Steps module Idv class Step - def initialize(idv_form:, idv_session:, params:) - @idv_form = idv_form + def initialize(idv_session:, idv_form_params:, vendor_validator_result:) + @idv_form_params = idv_form_params @idv_session = idv_session - @params = params + @vendor_validator_result = vendor_validator_result end - def form_valid? - form_validate(params) + def vendor_validation_passed? + vendor_validator_result.success? + end + + def vendor_validation_timed_out? + vendor_validator_result.timed_out? end private attr_accessor :idv_session - attr_reader :idv_form, :params - - def form_validate(params) - @form_result ||= idv_form.submit(params) - end - - def vendor_validation_passed? - vendor_validator.success? - end + attr_reader :idv_form_params, :vendor_validator_result def errors - errors = idv_form.errors.messages.dup - return errors unless form_valid? && vendor_errors - merge_vendor_errors(errors) - end - - def merge_vendor_errors(errors) - vendor_errors.each_with_object(errors) do |(key, value), errs| - value = [value] unless value.is_a?(Array) - errs[key] = value + @_errors ||= begin + vendor_validator_result.errors.each_with_object({}) do |(key, value), errs| + errs[key] = Array(value) + end end end - - def vendor_errors - @_vendor_errors ||= vendor_validator.errors - end - - def vendor_validator - @_vendor_validator ||= vendor_validator_class.new( - idv_session: idv_session, - vendor_params: vendor_params - ) - end end end diff --git a/app/services/idv/usps_mail.rb b/app/services/idv/usps_mail.rb index dadb08f4b73..26b9193049f 100644 --- a/app/services/idv/usps_mail.rb +++ b/app/services/idv/usps_mail.rb @@ -12,6 +12,10 @@ def mail_spammed? max_events? && updated_within_last_month? end + def any_mail_sent? + user_mail_events.any? + end + private attr_reader :current_user diff --git a/app/services/idv/vendor_result.rb b/app/services/idv/vendor_result.rb new file mode 100644 index 00000000000..a6975a50c21 --- /dev/null +++ b/app/services/idv/vendor_result.rb @@ -0,0 +1,32 @@ +module Idv + class VendorResult + attr_reader :success, :errors, :reasons, :session_id, :normalized_applicant, :timed_out + + def self.new_from_json(json) + parsed = JSON.parse(json, symbolize_names: true) + + applicant = parsed[:normalized_applicant] + parsed[:normalized_applicant] = Proofer::Applicant.new(applicant) if applicant + + new(**parsed) + end + + def initialize(success: nil, errors: {}, reasons: [], session_id: nil, + normalized_applicant: nil, timed_out: nil) + @success = success + @errors = errors + @reasons = reasons + @session_id = session_id + @normalized_applicant = normalized_applicant + @timed_out = timed_out + end + + def success? + success + end + + def timed_out? + timed_out + end + end +end diff --git a/app/services/idv/vendor_validator.rb b/app/services/idv/vendor_validator.rb index bb0ca462b9e..8b0c6222101 100644 --- a/app/services/idv/vendor_validator.rb +++ b/app/services/idv/vendor_validator.rb @@ -1,35 +1,28 @@ # abstract base class for proofing vendor validation module Idv class VendorValidator - delegate :success?, :errors, to: :result - attr_reader :idv_session, :vendor_params + attr_reader :applicant, :vendor, :vendor_params, :vendor_session_id - def initialize(idv_session:, vendor_params:) - @idv_session = idv_session + def initialize(applicant:, vendor:, vendor_params:, vendor_session_id:) + @applicant = applicant + @vendor = vendor @vendor_params = vendor_params + @vendor_session_id = vendor_session_id end - def reasons - result.vendor_resp.reasons + def result + @_result ||= try_submit end private - def idv_vendor - @_idv_vendor ||= Idv::Vendor.new - end - def idv_agent @_agent ||= Idv::Agent.new( - applicant: idv_session.applicant, - vendor: (idv_session.vendor || idv_vendor.pick) + applicant: applicant, + vendor: vendor ) end - def result - @_result ||= try_submit - end - def try_agent_action yield rescue => err diff --git a/app/services/locale_chooser.rb b/app/services/locale_chooser.rb new file mode 100644 index 00000000000..8b8676e6a29 --- /dev/null +++ b/app/services/locale_chooser.rb @@ -0,0 +1,21 @@ +class LocaleChooser + include HttpAcceptLanguage::EasyAccess + + def initialize(locale_param, request) + @locale_param = locale_param + @request = request + end + + def locale + return locale_param if locale_valid? + http_accept_language.compatible_language_from(I18n.available_locales) || I18n.default_locale + end + + private + + attr_reader :locale_param, :request + + def locale_valid? + LocaleValidator.new(locale_param).success? + end +end diff --git a/app/services/locale_validator.rb b/app/services/locale_validator.rb new file mode 100644 index 00000000000..b1e6f1bb59b --- /dev/null +++ b/app/services/locale_validator.rb @@ -0,0 +1,13 @@ +class LocaleValidator + def initialize(locale) + @locale = locale + end + + def success? + locale.present? && I18n.available_locales.include?(locale.to_sym) + end + + private + + attr_reader :locale +end diff --git a/app/services/marketing_site.rb b/app/services/marketing_site.rb index ccbfa81b344..776fdb339cd 100644 --- a/app/services/marketing_site.rb +++ b/app/services/marketing_site.rb @@ -1,23 +1,28 @@ class MarketingSite BASE_URL = URI('https://www.login.gov').freeze + def self.locale_segment + active_locale = I18n.locale + active_locale == I18n.default_locale ? '/' : "/#{active_locale}/" + end + def self.base_url - BASE_URL.to_s + URI.join(BASE_URL, locale_segment).to_s end def self.privacy_url - URI.join(BASE_URL, '/policy').to_s + URI.join(BASE_URL, locale_segment, 'policy').to_s end def self.contact_url - URI.join(BASE_URL, '/contact').to_s + URI.join(BASE_URL, locale_segment, 'contact').to_s end def self.help_url - URI.join(BASE_URL, '/help').to_s + URI.join(BASE_URL, locale_segment, 'help').to_s end def self.help_authenticator_app_url - URI.join(BASE_URL, '/help/signing-in/what-is-an-authenticator-app/').to_s + URI.join(BASE_URL, locale_segment, 'help/signing-in/what-is-an-authenticator-app/').to_s end end diff --git a/app/services/null_service_provider_request.rb b/app/services/null_service_provider_request.rb index ed799a2bbbc..6d5f732b755 100644 --- a/app/services/null_service_provider_request.rb +++ b/app/services/null_service_provider_request.rb @@ -4,4 +4,10 @@ def uuid; end def issuer; end def delete; end + + def url; end + + def loa; end + + def requested_attributes; end end diff --git a/app/services/otp_rate_limiter.rb b/app/services/otp_rate_limiter.rb index 1fb7bc29dcf..b0287467a20 100644 --- a/app/services/otp_rate_limiter.rb +++ b/app/services/otp_rate_limiter.rb @@ -32,10 +32,8 @@ def lock_out_user end def increment - now = Time.zone.now - entry_for_current_phone.otp_send_count += 1 - entry_for_current_phone.otp_last_sent_at = now + entry_for_current_phone.otp_last_sent_at = Time.zone.now entry_for_current_phone.save! end diff --git a/app/services/phone_formatter.rb b/app/services/phone_formatter.rb new file mode 100644 index 00000000000..50f9a376116 --- /dev/null +++ b/app/services/phone_formatter.rb @@ -0,0 +1,12 @@ +class PhoneFormatter + DEFAULT_COUNTRY = 'US'.freeze + + def format(phone, country_code: nil) + normalized_phone = if country_code + phone&.phony_normalized(country_code: country_code) + else + phone&.phony_normalized(default_country_code: DEFAULT_COUNTRY) + end + normalized_phone&.phony_formatted(format: :international, spaces: ' ') + end +end diff --git a/app/services/phone_number_capabilities.rb b/app/services/phone_number_capabilities.rb new file mode 100644 index 00000000000..559bf217253 --- /dev/null +++ b/app/services/phone_number_capabilities.rb @@ -0,0 +1,73 @@ +class PhoneNumberCapabilities + VOICE_UNSUPPORTED_US_AREA_CODES = { + '648' => 'American Samoa', + '264' => 'Anguilla', + '268' => 'Antigua and Barbuda', + '246' => 'Barbados', + '441' => 'Bermuda', + '345' => 'Cayman Islands', + '767' => 'Dominica', + '809' => 'Dominican Republic', + '473' => 'Grenada', + '671' => 'Guam', + '876' => 'Jamaica', + '670' => 'Northern Mariana Islands', + '869' => 'Saint Kitts and Nevis', + '758' => 'Saint Lucia', + '784' => 'Saint Vincent Grenadines', + '868' => 'Trinidad and Tobago', + '649' => 'Turks and Caicos Islands', + '284' => 'British Virgin Islands', + '340' => 'United States Virgin Islands', + }.freeze + + INTERNATIONAL_CODES = YAML.load_file( + Rails.root.join('config', 'country_dialing_codes.yml') + ).freeze + + attr_reader :phone + + def initialize(phone) + @phone = phone + end + + def sms_only? + if international_code == '1' + VOICE_UNSUPPORTED_US_AREA_CODES[area_code].present? + elsif country_code_data + country_code_data['sms_only'] + end + end + + def unsupported_location + if international_code == '1' + VOICE_UNSUPPORTED_US_AREA_CODES[area_code] + elsif country_code_data + country_code_data['name'] + end + end + + private + + def area_code + @area_code ||= phone_number_components.second + end + + def country_code_data + @country_code_data ||= INTERNATIONAL_CODES.select do |_, value| + value['country_code'] == international_code + end.values.first + end + + def international_code + @international_code ||= phone_number_components.first + end + + def phone_number_components + return [] if phone.blank? + + @phone_number_components ||= Phony.split( + PhonyRails.normalize_number(phone.to_s, default_country_code: :us).slice(1..-1) + ) + end +end diff --git a/app/services/store_sp_metadata_in_session.rb b/app/services/store_sp_metadata_in_session.rb index 785c2a101eb..120c9d3e044 100644 --- a/app/services/store_sp_metadata_in_session.rb +++ b/app/services/store_sp_metadata_in_session.rb @@ -19,7 +19,7 @@ def call attr_reader :session, :request_id def sp_request - @sp_request ||= ServiceProviderRequest.find_by(uuid: request_id) + @sp_request ||= ServiceProviderRequest.from_uuid(request_id) end def loa3_requested? diff --git a/app/services/submit_idv_job.rb b/app/services/submit_idv_job.rb new file mode 100644 index 00000000000..17f932b0870 --- /dev/null +++ b/app/services/submit_idv_job.rb @@ -0,0 +1,37 @@ +class SubmitIdvJob + def initialize(vendor_validator_class:, idv_session:, vendor_params:) + @vendor_validator_class = vendor_validator_class + @idv_session = idv_session + @vendor_params = vendor_params + end + + def call + update_idv_session + + VendorValidatorJob.perform_later( + result_id: result_id, + vendor_validator_class: vendor_validator_class.to_s, + vendor: vendor.to_s, + vendor_params: vendor_params, + vendor_session_id: idv_session.vendor_session_id, + applicant_json: idv_session.applicant.to_json + ) + end + + private + + attr_reader :vendor_validator_class, :idv_session, :vendor_params + + def result_id + @_result_id ||= SecureRandom.uuid + end + + def update_idv_session + idv_session.async_result_id = result_id + idv_session.async_result_started_at = Time.zone.now.to_i + end + + def vendor + idv_session.vendor || Idv::Vendor.new.pick + end +end diff --git a/app/services/usps_confirmation_maker.rb b/app/services/usps_confirmation_maker.rb index 1fb164a1597..e9a3ddf8af5 100644 --- a/app/services/usps_confirmation_maker.rb +++ b/app/services/usps_confirmation_maker.rb @@ -17,14 +17,14 @@ def perform # This method is single statement spread across many lines for readability def attributes { - address1: pii[:address1], - address2: pii[:address2], - city: pii[:city], - otp: pii[:otp], - first_name: pii[:first_name], - last_name: pii[:last_name], - state: pii[:state], - zipcode: pii[:zipcode], + address1: pii[:address1].norm, + address2: pii[:address2].norm, + city: pii[:city].norm, + otp: pii[:otp].norm, + first_name: pii[:first_name].norm, + last_name: pii[:last_name].norm, + state: pii[:state].norm, + zipcode: pii[:zipcode].norm, issuer: issuer, } end diff --git a/app/services/usps_uploader.rb b/app/services/usps_uploader.rb new file mode 100644 index 00000000000..6b0749a07ae --- /dev/null +++ b/app/services/usps_uploader.rb @@ -0,0 +1,43 @@ +class UspsUploader + def run + build_file + upload_file + clear_file + rescue => error + NewRelic::Agent.notice_error(error) + end + + # @api private + def local_path + @_local_path ||= begin + timestamp = Time.zone.now.strftime('%Y%m%d%H%M%S') + Rails.root.join('tmp', "batch-#{timestamp}.pgp") + end + end + + private + + def build_file + UspsExporter.new(local_path).run + end + + def upload_file + env = Figaro.env + + Net::SFTP.start( + env.equifax_sftp_host, + env.equifax_sftp_username, + key_data: [RequestKeyManager.equifax_ssh_key.to_pem] + ) do |sftp| + sftp.upload!(local_path.to_s, remote_path) + end + end + + def clear_file + FileUtils.rm(local_path) + end + + def remote_path + File.join(Figaro.env.equifax_sftp_directory, 'batch.pgp') + end +end diff --git a/app/services/vendor_validator_result_storage.rb b/app/services/vendor_validator_result_storage.rb new file mode 100644 index 00000000000..098ba2422ff --- /dev/null +++ b/app/services/vendor_validator_result_storage.rb @@ -0,0 +1,24 @@ +class VendorValidatorResultStorage + TTL = Figaro.env.session_timeout_in_minutes.to_i.minutes.seconds.to_i + + def store(result_id:, result:) + Sidekiq.redis do |redis| + redis.setex(redis_key(result_id), TTL, result.to_json) + end + end + + def load(result_id) + result_json = Sidekiq.redis do |redis| + redis.get(redis_key(result_id)) + end + + return unless result_json + + Idv::VendorResult.new_from_json(result_json) + end + + # @api private + def redis_key(result_id) + "vendor-validator-result-#{result_id}" + end +end diff --git a/app/validators/form_password_validator.rb b/app/validators/form_password_validator.rb index 23c6b9a9a4f..6997842b5a5 100644 --- a/app/validators/form_password_validator.rb +++ b/app/validators/form_password_validator.rb @@ -40,11 +40,15 @@ def zxcvbn_feedback feedback = @pass_score.feedback.values.flatten.reject(&:empty?) feedback.map do |error| - I18n.t("zxcvbn.feedback.#{error.tr('.', '_')}") + I18n.t("zxcvbn.feedback.#{i18n_key(error)}") end.join('. ').gsub(/\.\s*\./, '.') end def password_strength_enabled? @enabled ||= FeatureManagement.password_strength_enabled? end + + def i18n_key(key) + key.tr(' -', '_').gsub(/\W/, '').downcase + end end diff --git a/app/validators/form_phone_validator.rb b/app/validators/form_phone_validator.rb index eef673377a3..164b12bfc57 100644 --- a/app/validators/form_phone_validator.rb +++ b/app/validators/form_phone_validator.rb @@ -3,8 +3,11 @@ module FormPhoneValidator included do validates_plausible_phone :phone, - country_code: 'US', presence: true, - message: :improbable_phone + message: :improbable_phone, + international_code: ->(form) { form.international_code } + validates :international_code, inclusion: { + in: PhoneNumberCapabilities::INTERNATIONAL_CODES.keys, + } end end diff --git a/app/validators/otp_delivery_preference_validator.rb b/app/validators/otp_delivery_preference_validator.rb new file mode 100644 index 00000000000..b6f8ae643e2 --- /dev/null +++ b/app/validators/otp_delivery_preference_validator.rb @@ -0,0 +1,20 @@ +module OtpDeliveryPreferenceValidator + extend ActiveSupport::Concern + + included do + validate :otp_delivery_preference_supported + end + + def otp_delivery_preference_supported + capabilities = PhoneNumberCapabilities.new(phone) + return unless otp_delivery_preference == 'voice' && capabilities.sms_only? + + errors.add( + :phone, + I18n.t( + 'devise.two_factor_authentication.otp_delivery_preference.phone_unsupported', + location: capabilities.unsupported_location + ) + ) + end +end diff --git a/app/view_models/verify/base.rb b/app/view_models/verify/base.rb index 84e4bd669ef..46d0c291aa0 100644 --- a/app/view_models/verify/base.rb +++ b/app/view_models/verify/base.rb @@ -2,16 +2,17 @@ module Verify class Base include Rails.application.routes.url_helpers - def initialize(error: nil, remaining_attempts:, idv_form:) + def initialize(error: nil, remaining_attempts:, idv_form:, timed_out: nil) @error = error @remaining_attempts = remaining_attempts @idv_form = idv_form + @timed_out = timed_out end attr_reader :error, :remaining_attempts, :idv_form def mock_vendor_partial - if idv_vendor.pick == :mock + if FeatureManagement.no_pii_mode? 'verify/sessions/no_pii_warning' else 'shared/null' @@ -39,6 +40,7 @@ def warning_partial end def message + return html_paragraph(text: I18n.t("idv.modal.#{step_name}.timeout")) if timed_out? html_paragraph(text: I18n.t("idv.modal.#{step_name}.#{error}")) if error end @@ -56,14 +58,14 @@ def flash_message flash_heading = html_paragraph( text: I18n.t("idv.modal.#{step_name}.heading"), css_class: 'mb2 fs-20p' ) - flash_body = html_paragraph(text: I18n.t("idv.modal.#{step_name}.#{error}")) + flash_body = message flash_heading + flash_body + attempts end private - def idv_vendor - @_idv_vendor ||= Idv::Vendor.new + def timed_out? + @timed_out end def button_link_text diff --git a/app/views/devise/mailer/confirmation_instructions.html.slim b/app/views/devise/mailer/confirmation_instructions.html.slim index f35fa928af9..e95edad6caf 100644 --- a/app/views/devise/mailer/confirmation_instructions.html.slim +++ b/app/views/devise/mailer/confirmation_instructions.html.slim @@ -12,15 +12,15 @@ table.button.expanded.large.radius center = link_to t('mailer.confirmation_instructions.link_text'), \ sign_up_create_email_confirmation_url(_request_id: \ - @request_id, confirmation_token: @token), \ + @request_id, confirmation_token: @token, locale: @locale), \ target: '_blank', \ class: 'float-center', align: 'center' td.expander p = link_to sign_up_create_email_confirmation_url(_request_id: @request_id, \ - confirmation_token: @token), \ + confirmation_token: @token, locale: @locale), \ sign_up_create_email_confirmation_url(_request_id: @request_id, \ - confirmation_token: @token), target: '_blank' + confirmation_token: @token, locale: @locale), target: '_blank' table.spacer tbody diff --git a/app/views/devise/mailer/reset_password_instructions.html.slim b/app/views/devise/mailer/reset_password_instructions.html.slim index a20f9ed0ede..71674e9e400 100644 --- a/app/views/devise/mailer/reset_password_instructions.html.slim +++ b/app/views/devise/mailer/reset_password_instructions.html.slim @@ -11,13 +11,13 @@ table.button.expanded.large.radius td center = link_to t('mailer.reset_password.link_text'), - edit_password_url(@resource, reset_password_token: @token), + edit_password_url(@resource, reset_password_token: @token, locale: @locale), target: '_blank', class: 'float-center', align: 'center' td.expander p - = link_to edit_password_url(@resource, reset_password_token: @token), \ - edit_password_url(@resource, reset_password_token: @token), \ + = link_to edit_password_url(@resource, reset_password_token: @token, locale: @locale), \ + edit_password_url(@resource, reset_password_token: @token, locale: @locale), \ target: '_blank' table.spacer diff --git a/app/views/devise/passwords/new.html.slim b/app/views/devise/passwords/new.html.slim index 345c3eab860..52b74d9aaee 100644 --- a/app/views/devise/passwords/new.html.slim +++ b/app/views/devise/passwords/new.html.slim @@ -10,4 +10,6 @@ p.mt-tiny.mb0#email-description = f.input :email, required: true, input_html: { 'aria-describedby': 'email-description' } = f.button :submit, t('forms.buttons.continue'), class: 'mt2' -= render 'shared/cancel', link: new_user_session_path +.mt2.pt1.border-top + = link_to t('links.cancel'), decorated_session.cancel_link_path, class: 'h5' + diff --git a/app/views/layouts/application.html.slim b/app/views/layouts/application.html.slim index 89384a00d1f..ccac1c14bb4 100644 --- a/app/views/layouts/application.html.slim +++ b/app/views/layouts/application.html.slim @@ -8,6 +8,8 @@ html lang="#{I18n.locale}" class='no-js' meta name='msapplication-config' content='none' meta[name='viewport' content='width=device-width, initial-scale=1.0'] meta name="format-detection" content="telephone=no" + - if content_for?(:meta_refresh) + meta http-equiv="refresh" content="#{yield(:meta_refresh)}" title = content_for?(:title) ? APP_NAME + ' - ' + yield(:title) : APP_NAME @@ -45,6 +47,7 @@ html lang="#{I18n.locale}" class='no-js' body class="#{Rails.env}-env site sm-bg-light-blue" .site-wrap = render 'shared/i18n_mode' if FeatureManagement.enable_i18n_mode? + = render 'shared/no_pii_banner' if FeatureManagement.no_pii_mode? = render 'shared/usa_banner' - if content_for?(:nav) = yield(:nav) diff --git a/app/views/shared/_accordion.html.slim b/app/views/shared/_accordion.html.slim index b36a57c2bab..e42dfed41b3 100644 --- a/app/views/shared/_accordion.html.slim +++ b/app/views/shared/_accordion.html.slim @@ -18,6 +18,7 @@ .accordion-content.clearfix.pt1( id="#{target_id}" role="region" + aria-hidden="true" ) .px2 = yield diff --git a/app/views/shared/_footer_lite.html.slim b/app/views/shared/_footer_lite.html.slim index ca77f40ade4..d1a5983e6ee 100644 --- a/app/views/shared/_footer_lite.html.slim +++ b/app/views/shared/_footer_lite.html.slim @@ -1,16 +1,45 @@ +- show_language_dropdown = I18n.available_locales.count > 1 + footer.footer.bg-light-blue.sm-bg-navy - .container.h6.py1.px2.lg-px0 - .clearfix - .col-right.caps + - if show_language_dropdown + .sm-hide.border-bottom + .container.py1.px2.lg-px0.h5 + .center + span.i18n-mobile-toggle.block.text-decoration-none.blue.fs-13p + = image_tag asset_url('globe-blue.svg'), width: 12, class: 'mr1', alt: '' + = t('i18n.language') + span.caret.inline-block.ml-tiny + | ▾ + .i18n-mobile-dropdown.sm-hide + ul.list-reset.mb0.white.center + - I18n.available_locales.each do |locale| + li.border-bottom + = link_to t("i18n.locale.#{locale}"), { locale: locale }, + class: 'block py-12p px2 text-decoration-none blue fs-13p' + .container.py1.px2.lg-px0(class="#{'sm-py0' if show_language_dropdown}") + .flex.flex-center + .flex.flex-center + - if show_language_dropdown + .i18n-desktop-toggle.sm-show.white.my1.mr3.px1.py-tiny.border.border-blue.rounded-lg + = image_tag asset_url('globe-white.svg'), width: 12, class: 'mr1', alt: '' + = t('i18n.language') + span.caret.inline-block.ml-tiny + | ▾ + .i18n-desktop-dropdown + ul.list-reset.mb0.white + - I18n.available_locales.each do |locale| + li.border-bottom.border-navy + = link_to t("i18n.locale.#{locale}"), { locale: locale }, + class: 'block pl-24p py2 text-decoration-none white' = link_to t('links.help'), MarketingSite.help_url, - class: 'gray sm-white text-decoration-none mr3', target: '_blank' + class: 'caps h6 blue sm-white text-decoration-none mr3', target: '_blank' = link_to t('links.contact'), MarketingSite.contact_url, - class: 'gray sm-white text-decoration-none mr3', target: '_blank' + class: 'caps h6 blue sm-white text-decoration-none mr3', target: '_blank' = link_to t('links.privacy_policy'), MarketingSite.privacy_url, - class: 'gray sm-white text-decoration-none', target: '_blank' - .col + class: 'caps h6 blue sm-white text-decoration-none', target: '_blank' + .flex.flex-auto.flex-first = link_to('https://gsa.gov', - class: 'flex flex-center text-decoration-none white', + class: 'flex flex-center text-decoration-none white h6', target: '_blank') do = image_tag asset_url('sp-logos/square-gsa.svg'), width: 20, class: 'mr1 sm-show', alt: 'GSA homepage' @@ -18,4 +47,3 @@ footer.footer.bg-light-blue.sm-bg-navy width: 20, class: 'mr1 sm-hide', alt: 'GSA homepage' span.sm-show = t('shared.footer_lite.gsa') - end diff --git a/app/views/shared/_no_pii_banner.html.slim b/app/views/shared/_no_pii_banner.html.slim new file mode 100644 index 00000000000..9e93f447010 --- /dev/null +++ b/app/views/shared/_no_pii_banner.html.slim @@ -0,0 +1,2 @@ +.py1.bg-maroon.white.fs-12p.line-height-1.center + = t('idv.messages.sessions.no_pii') diff --git a/app/views/shared/_pii_review.html.slim b/app/views/shared/_pii_review.html.slim index 13b5ae8d0fd..a058ba8531a 100644 --- a/app/views/shared/_pii_review.html.slim +++ b/app/views/shared/_pii_review.html.slim @@ -12,8 +12,9 @@ .mt3.h6 = t('idv.review.ssn') .h4.bold.ico-absolute.ico-absolute-success #{pii[:ssn]} - .h6.mt3 = t('idv.messages.phone.phone_of_record') - .h4.bold.ico-absolute.ico-absolute-success #{pii[:phone]} + - if phone + .h6.mt3 = t('idv.messages.phone.phone_of_record') + .h4.bold.ico-absolute.ico-absolute-success #{phone} .mt3.mb3 = link_to t('idv.messages.review.financial_info'), MarketingSite.help_url diff --git a/app/views/shared/refresh.html.slim b/app/views/shared/refresh.html.slim new file mode 100644 index 00000000000..6c7ef0103ec --- /dev/null +++ b/app/views/shared/refresh.html.slim @@ -0,0 +1,7 @@ +- content_for(:meta_refresh) do + = Figaro.env.async_job_refresh_interval_seconds.to_s + +h2 = t('idv.messages.loading') + +.loading-spinner + img src="#{image_path('spinner.gif')}" height=100 width=100 alt="" diff --git a/app/views/users/phones/edit.html.slim b/app/views/users/phones/edit.html.slim index 3da297496fd..73ba00505a9 100644 --- a/app/views/users/phones/edit.html.slim +++ b/app/views/users/phones/edit.html.slim @@ -2,8 +2,15 @@ h1.h3.my0 = t('headings.edit_info.phone') -= simple_form_for(@update_user_phone_form, url: manage_phone_path, += simple_form_for(@update_user_phone_form, + data: { unsupported_area_codes: @unsupported_area_codes, + international_phone_form: true }, + url: manage_phone_path, html: { autocomplete: 'off', method: :put, role: 'form' }) do |f| + = f.input :international_code, + collection: international_phone_codes, + include_blank: false, + input_html: { class: 'international-code' } = f.input :phone, as: :tel, required: true, input_html: { class: 'phone', value: nil }, label: t('account.index.phone') = f.button :submit, t('forms.buttons.submit.confirm_change'), class: 'mt2' diff --git a/app/views/users/totp_setup/new.html.slim b/app/views/users/totp_setup/new.html.slim index 1c5136488ee..7401123403a 100644 --- a/app/views/users/totp_setup/new.html.slim +++ b/app/views/users/totp_setup/new.html.slim @@ -22,8 +22,8 @@ ul.list-reset .clipboard.ml2.right.mt-tiny class=btn_cls data-clipboard-text=@code.upcase = t('links.copy') .py2.center.bold - = t('instructions.2fa.authenticator.or') - = accordion('totp-info', t('instructions.2fa.authenticator.accordion_header'), + = t('instructions.mfa.authenticator.or') + = accordion('totp-info', t('instructions.mfa.authenticator.accordion_header'), wrapper_css: 'mb2 col-12 fs-16p') do .center = image_tag @qrcode, alt: t('users.totp_setup.new.qr_img_alt') diff --git a/app/views/users/two_factor_authentication_setup/index.html.slim b/app/views/users/two_factor_authentication_setup/index.html.slim index 51be3136fbf..2a86ed90b15 100644 --- a/app/views/users/two_factor_authentication_setup/index.html.slim +++ b/app/views/users/two_factor_authentication_setup/index.html.slim @@ -5,11 +5,20 @@ p.mt-tiny.mb0 = t('devise.two_factor_authentication.otp_setup_html') = simple_form_for(@two_factor_setup_form, html: { autocomplete: 'off', role: 'form' }, + data: { unsupported_area_codes: @unsupported_area_codes, + international_phone_form: true }, method: :patch, url: phone_setup_path) do |f| + .clearfix + .sm-col.sm-col-8 + = f.input :international_code, + collection: international_phone_codes, + include_blank: false, + input_html: { class: 'international-code' } = f.label :phone, class: 'block' strong.left = t('devise.two_factor_authentication.otp_phone_label') - span.ml1.italic = t('devise.two_factor_authentication.otp_phone_label_info') + span#otp_phone_label_info.ml1.italic + = t('devise.two_factor_authentication.otp_phone_label_info') = f.input :phone, as: :tel, label: false, required: true, input_html: { class: 'phone sm-col-8 mb4' } .mb3 @@ -25,7 +34,8 @@ p.mt-tiny.mb0 = radio_button_tag 'two_factor_setup_form[otp_delivery_preference]', :voice span.indicator = t('devise.two_factor_authentication.otp_delivery_preference.voice') - p.mt1.mb0 = t('devise.two_factor_authentication.otp_delivery_preference.instruction') + p#otp_delivery_preference_instruction.mt1.mb0 + = t('devise.two_factor_authentication.otp_delivery_preference.instruction') = f.button :submit, t('forms.buttons.send_security_code') = render 'shared/cancel', link: destroy_user_session_path diff --git a/app/views/users/verify_password/new.html.slim b/app/views/users/verify_password/new.html.slim index 20ea38eab25..a3bd8f130b0 100644 --- a/app/views/users/verify_password/new.html.slim +++ b/app/views/users/verify_password/new.html.slim @@ -6,4 +6,5 @@ h1.h3.my0 = t('idv.titles.session.review') .mt4 = accordion('review-verified-info', t('idv.messages.review.intro')) do - = render 'shared/pii_review', pii: @verify_password_form.decrypted_pii + - decrypted_pii = @verify_password_form.decrypted_pii + = render 'shared/pii_review', pii: decrypted_pii, phone: decrypted_pii[:phone].raw diff --git a/app/views/verify/review/new.html.slim b/app/views/verify/review/new.html.slim index 4cd7d7768d6..46e83ad6066 100644 --- a/app/views/verify/review/new.html.slim +++ b/app/views/verify/review/new.html.slim @@ -6,4 +6,6 @@ h1.h3.my0 = t('idv.titles.session.review') .mt4 = accordion('review-verified-info', t('idv.messages.review.intro')) do - = render 'shared/pii_review', pii: @idv_params + - phone = @idv_params[:phone] + - formatted_phone = PhoneFormatter.new.format(phone) + = render 'shared/pii_review', pii: @idv_params, phone: formatted_phone diff --git a/app/views/verify/usps/index.html.slim b/app/views/verify/usps/index.html.slim index 75c25ae8732..302048a8ad2 100644 --- a/app/views/verify/usps/index.html.slim +++ b/app/views/verify/usps/index.html.slim @@ -2,7 +2,7 @@ = image_tag(asset_url('check-email.svg'), size: '48x48', alt: 'check email',\ class: 'absolute top-n24 left-0 right-0 mx-auto') h1.h2 - = @title + = @decorated_usps.title p = t('idv.messages.usps.byline') @@ -10,7 +10,7 @@ strong = t('idv.messages.usps.success') -= button_to @button, verify_usps_path, method: 'put', += button_to @decorated_usps.button, verify_usps_path, method: 'put', class: 'btn btn-primary btn-wide', form_class: 'inline-block mr2' = link_to t('idv.messages.usps.bad_address'), verify_phone_path diff --git a/certs/sp/cbp_goes_prod.crt b/certs/sp/cbp_goes_prod.crt new file mode 100644 index 00000000000..5bf2246006b --- /dev/null +++ b/certs/sp/cbp_goes_prod.crt @@ -0,0 +1,45 @@ +-----BEGIN CERTIFICATE----- +MIIH8DCCBtigAwIBAgIEWRoIeDANBgkqhkiG9w0BAQsFADCBhzELMAkGA1UEBhMC +VVMxGDAWBgNVBAoTD1UuUy4gR292ZXJubWVudDEoMCYGA1UECxMfRGVwYXJ0bWVu +dCBvZiBIb21lbGFuZCBTZWN1cml0eTEiMCAGA1UECxMZQ2VydGlmaWNhdGlvbiBB +dXRob3JpdGllczEQMA4GA1UECxMHREhTIENBNDAeFw0xNzA2MjQxNDQ2MTlaFw0y +MDA2MjQxNTE2MTlaMIGPMQswCQYDVQQGEwJVUzEYMBYGA1UEChMPVS5TLiBHb3Zl +cm5tZW50MSgwJgYDVQQLEx9EZXBhcnRtZW50IG9mIEhvbWVsYW5kIFNlY3VyaXR5 +MQwwCgYDVQQLEwNDQlAxEDAOBgNVBAsTB0RldmljZXMxHDAaBgNVBAMTE3R0cC1h +cHAuY2JwLmRocy5nb3YwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDP +ubSOea85FVVr9Adn+mez7LjuPBs3aeMCEK9gM9xnV2tGFJra1Mh9ef/jnnO2ANur +jtRovws/ea/k+J54ngIv7ZXCZGUvZZoOtyFqX7Mjnj8gFvIrFCVAD4a/FySSiGNo +Z7+X/ypX1rFb8CNFSi/SxU+zH61ZS0i+OAZ2xAk3Gv+OkZ4DHRHXsGJn8rCJ2O1N +c/OyKpBOpkS5EjTAPw3OqD/U8CU9Hl6QbK52ovxFLgtkHGWv37dLc0Qojwa8Lqaa +FAxjWPyND2oo4aDfGtu7YtbGk0zRf97QNvfoqjkGYAaUK0ozCTMKZEVuXknhDtvy +a6C26kDVYA8RUn9RtFjNAgMBAAGjggRYMIIEVDAOBgNVHQ8BAf8EBAMCBaAwFwYD +VR0gBBAwDjAMBgpghkgBZQMCAQMIMIIBGwYIKwYBBQUHAQEEggENMIIBCTA0Bggr +BgEFBQcwAoYoaHR0cDovL3BraS5kaW1jLmRocy5nb3YvZGhzY2FfZWVfYWlhLnA3 +YzCBqgYIKwYBBQUHMAKGgZ1sZGFwOi8vbGRhcDAxLmRpbWMuZGhzLmdvdi9vdT1E +SFMlMjBDQTQsb3U9Q2VydGlmaWNhdGlvbiUyMEF1dGhvcml0aWVzLG91PURlcGFy +dG1lbnQlMjBvZiUyMEhvbWVsYW5kJTIwU2VjdXJpdHksbz1VLlMuJTIwR292ZXJu +bWVudCxjPVVTP2NBQ2VydGlmaWNhdGU7YmluYXJ5MCQGCCsGAQUFBzABhhhodHRw +Oi8vb2NzcC5kaW1jLmRocy5nb3YwEwYDVR0lBAwwCgYIKwYBBQUHAwEwggEBBgNV +HREEgfkwgfaCE3R0cC1hcHAuY2JwLmRocy5nb3aCGHR0cC1pbnRlcm5hbC5jYnAu +ZGhzLmdvdoIcdHRwLWludGVybmFsLWFwcC5jYnAuZGhzLmdvdoIZdHRwLXNjaGVk +dWxlci5jYnAuZGhzLmdvdoIddHRwLXNjaGVkdWxlci1hcHAuY2JwLmRocy5nb3aC +F3R0cC1jcmVkc3ZjLmNicC5kaHMuZ292ght0dHAtY3JlZHN2Yy1hcHAuY2JwLmRo +cy5nb3aCF3R0cC1hZHMtYXBwLmNicC5kaHMuZ292gh50dHAtc2FwYWRhcHRlci1h +cHAuY2JwLmRocy5nb3YwggGTBgNVHR8EggGKMIIBhjCCAVegggFToIIBT6SBnDCB +mTELMAkGA1UEBhMCVVMxGDAWBgNVBAoTD1UuUy4gR292ZXJubWVudDEoMCYGA1UE +CxMfRGVwYXJ0bWVudCBvZiBIb21lbGFuZCBTZWN1cml0eTEiMCAGA1UECxMZQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdGllczEQMA4GA1UECxMHREhTIENBNDEQMA4GA1UE +AxMHQ1JMNTM5MYaBrWxkYXA6Ly9sZGFwMDEuZGltYy5kaHMuZ292L2NuPUNSTDUz +OTEsb3U9REhTJTIwQ0E0LG91PUNlcnRpZmljYXRpb24lMjBBdXRob3JpdGllcyxv +dT1EZXBhcnRtZW50JTIwb2YlMjBIb21lbGFuZCUyMFNlY3VyaXR5LG89VS5TLiUy +MEdvdmVybm1lbnQsYz1VUz9jZXJ0aWZpY2F0ZVJldm9jYXRpb25MaXN0MCmgJ6Al +hiNodHRwOi8vcGtpLmRpbWMuZGhzLmdvdi9ESFNfQ0EyLmNybDAfBgNVHSMEGDAW +gBR8w0pcuh82q4NRffTg5Q6QfxwTQTAdBgNVHQ4EFgQUIWVxsBQWw5oIGgPYwrvG +o7c35EowGQYJKoZIhvZ9B0EABAwwChsEVjguMQMCA6gwDQYJKoZIhvcNAQELBQAD +ggEBAG3uUeSo89FY4HcDGGhO9mBjXuk7HRHGLnY8fAdqJoT1gA/oTHoV/fgw8QZu +D3X9FLgpEjQz43iHFrv/ps2tYwgSKiiovoeYyb68s5X9NuG5kNnn0dGp1TjnNlGX +9lQSBr4CReaJpCtSAPbOtuZ+8b7MA7ODnMg/1u1qNKovIqMV6eIq5vrD/+MeSG61 ++c+MI6Gita5j6J6lzZqqwtAltfMFetc1Qkl2tjqdpce9u60Ch7yuVR3Xh1SEhaAa +jRJsbOuqNAfC038A6ASv3nxGao8AYUHt2aR9chXApKfBzfx6VYW0462UPqzeRi5l +RCa8sud1bb1pFRJv8LWiqOC/9yw= +-----END CERTIFICATE----- diff --git a/config/application.rb b/config/application.rb index b12f84bf8a2..91fbd99d570 100644 --- a/config/application.rb +++ b/config/application.rb @@ -16,8 +16,9 @@ class Application < Rails::Application config.browserify_rails.force = true config.browserify_rails.commandline_options = '-t [ babelify --presets [ es2015 ] ]' - config.i18n.available_locales = Figaro.env.available_locales.split(' ') config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{yml}')] + config.i18n.available_locales = Figaro.env.available_locales.split(' ') + config.i18n.default_locale = :en routes.default_url_options[:host] = Figaro.env.domain_name diff --git a/config/application.yml.example b/config/application.yml.example index 3d9d481d90c..753692138e3 100644 --- a/config/application.yml.example +++ b/config/application.yml.example @@ -49,11 +49,15 @@ use_dashboard_service_providers: 'false' dashboard_url: 'https://dashboard.demo.login.gov' valid_authn_contexts: '["http://idmanagement.gov/ns/assurance/loa/1", "http://idmanagement.gov/ns/assurance/loa/3"]' +usps_mail_batch_hours: '24' + development: + async_job_refresh_interval_seconds: '5' + async_job_refresh_max_wait_seconds: '15' attribute_cost: '4000$8$4$' # SCrypt::Engine.calibrate(max_time: 0.5) attribute_encryption_key: '2086dfbd15f5b0c584f3664422a1d3409a0d2aa6084f65b6ba57d64d4257431c124158670c7655e45cabe64194f7f7b6c7970153c285bdb8287ec0c4f7553e25' attribute_encryption_key_queue: '["old-key-one", "old-key-two"]' - available_locales: 'en es' + available_locales: 'en es fr' aws_kms_key_id: 'alias/login-dot-gov-development-keymaker' aws_region: 'us-east-1' dashboard_api_token: 'test_token' @@ -69,6 +73,9 @@ development: equifax_gpg_email: 'logs@login.gov' equifax_password: 'sekret' equifax_phone_username: 'sekret' + equifax_sftp_directory: '/directory' + equifax_sftp_host: 'example.com' + equifax_sftp_username: 'user' equifax_ssh_passphrase: 'sekret' hmac_fingerprinter_key: 'a2c813d4dca919340866ba58063e4072adc459b767a74cf2666d5c1eef3861db26708e7437abde1755eb24f4034386b0fea1850a1cb7e56bff8fae3cc6ade96c' hmac_fingerprinter_key_queue: '["old-key-one", "old-key-two"]' @@ -105,6 +112,8 @@ development: enable_load_testing_mode: 'false' production: + async_job_refresh_interval_seconds: '5' + async_job_refresh_max_wait_seconds: '15' attribute_cost: '4000$8$4$' # SCrypt::Engine.calibrate(max_time: 0.5) attribute_encryption_key: 'generate via `rake secret`' attribute_encryption_key_queue: '["old-key-one", "old-key-two"]' @@ -124,6 +133,9 @@ production: equifax_gpg_email: 'sekret@example.com' equifax_password: 'sekret' equifax_phone_username: 'sekret' + equifax_sftp_directory: '/directory' + equifax_sftp_host: 'example.com' + equifax_sftp_username: 'user' equifax_ssh_passphrase: 'sekret' google_analytics_key: 'UA-XXXXXXXXX-YY' hmac_fingerprinter_key: 'generate via `rake secret`' @@ -132,6 +144,7 @@ production: lockout_period_in_minutes: '10' logins_per_ip_limit: '20' logins_per_ip_period: '8' + mandrill_api_token: '123abc' newrelic_license_key: 'xxx' newrelic_browser_key: '' newrelic_browser_app_id: '' @@ -152,7 +165,6 @@ production: secret_key_base: 'generate via `rake secret`' session_encryption_key: 'generate via `rake secret`' session_timeout_in_minutes: '8' - smtp_settings: '{"address":"smtp.mandrillapp.com", "port":587, "authentication":"login", "enable_starttls_auto":true, "user_name":"user@gmail.com","password":"xxx"}' twilio_accounts: '[{"sid":"sid", "auth_token":"token", "number":"9999999999"}]' twilio_record_voice: 'false' use_kms: 'false' @@ -161,10 +173,12 @@ production: enable_load_testing_mode: 'false' test: + async_job_refresh_interval_seconds: '1' + async_job_refresh_max_wait_seconds: '15' attribute_cost: '800$8$1$' # SCrypt::Engine.calibrate(max_time: 0.01) attribute_encryption_key: '2086dfbd15f5b0c584f3664422a1d3409a0d2aa6084f65b6ba57d64d4257431c124158670c7655e45cabe64194f7f7b6c7970153c285bdb8287ec0c4f7553e25' attribute_encryption_key_queue: '["old-key-one", "old-key-two"]' - available_locales: 'en es' + available_locales: 'en es fr' aws_kms_key_id: 'alias/login-dot-gov-test-keymaker' aws_region: 'us-east-1' domain_name: 'www.example.com' @@ -180,6 +194,9 @@ test: equifax_gpg_email: 'logs@login.gov' equifax_password: 'sekret' equifax_phone_username: 'sekret' + equifax_sftp_directory: '/directory' + equifax_sftp_host: 'example.com' + equifax_sftp_username: 'user' equifax_ssh_passphrase: 'sekret' hmac_fingerprinter_key: 'a2c813d4dca919340866ba58063e4072adc459b767a74cf2666d5c1eef3861db26708e7437abde1755eb24f4034386b0fea1850a1cb7e56bff8fae3cc6ade96c' hmac_fingerprinter_key_queue: '["old-key-one", "old-key-two"]' diff --git a/config/country_dialing_codes.yml b/config/country_dialing_codes.yml new file mode 100644 index 00000000000..75013b7abdc --- /dev/null +++ b/config/country_dialing_codes.yml @@ -0,0 +1,804 @@ +US: + name: United States of America + country_code: '1' + sms_only: false +AF: + name: Afghanistan + country_code: '93' + sms_only: true +AL: + name: Albania + country_code: '355' + sms_only: true +DZ: + name: Algeria + country_code: '213' + sms_only: true +AD: + name: Andorra + country_code: '376' + sms_only: true +AO: + name: Angola + country_code: '244' + sms_only: true +AR: + name: Argentina + country_code: '54' + sms_only: true +AM: + name: Armenia + country_code: '374' + sms_only: true +AW: + name: Aruba + country_code: '297' + sms_only: true +AU: + name: Australia/Cocos/Christmas Island + country_code: '61' + sms_only: false +AT: + name: Austria + country_code: '43' + sms_only: false +AZ: + name: Azerbaijan + country_code: '994' + sms_only: true +BH: + name: Bahrain + country_code: '973' + sms_only: false +BD: + name: Bangladesh + country_code: '880' + sms_only: true +BY: + name: Belarus + country_code: '375' + sms_only: true +BE: + name: Belgium + country_code: '32' + sms_only: false +BZ: + name: Belize + country_code: '501' + sms_only: true +BJ: + name: Benin + country_code: '229' + sms_only: true +BT: + name: Bhutan + country_code: '975' + sms_only: true +BO: + name: Bolivia + country_code: '591' + sms_only: true +BA: + name: Bosnia and Herzegovina + country_code: '387' + sms_only: true +BW: + name: Botswana + country_code: '267' + sms_only: true +BR: + name: Brazil + country_code: '55' + sms_only: false +BN: + name: Brunei + country_code: '673' + sms_only: true +BG: + name: Bulgaria + country_code: '359' + sms_only: false +BF: + name: Burkina Faso + country_code: '226' + sms_only: true +BI: + name: Burundi + country_code: '257' + sms_only: true +KH: + name: Cambodia + country_code: '855' + sms_only: true +CM: + name: Cameroon + country_code: '237' + sms_only: true +CA: + name: Canada + country_code: '1' + sms_only: true +CV: + name: Cape Verde + country_code: '238' + sms_only: true +CF: + name: Central Africa + country_code: '236' + sms_only: true +TD: + name: Chad + country_code: '235' + sms_only: true +CL: + name: Chile + country_code: '56' + sms_only: true +CN: + name: China + country_code: '86' + sms_only: false +CO: + name: Colombia + country_code: '57' + sms_only: true +KM: + name: Comoros + country_code: '269' + sms_only: true +CG: + name: Congo + country_code: '242' + sms_only: true +CD: + name: Congo (Democratic Republic of the) + country_code: '243' + sms_only: true +CK: + name: Cook Islands + country_code: '682' + sms_only: true +CR: + name: Costa Rica + country_code: '506' + sms_only: true +HR: + name: Croatia + country_code: '385' + sms_only: true +CU: + name: Cuba + country_code: '53' + sms_only: true +CY: + name: Cyprus + country_code: '357' + sms_only: false +CZ: + name: Czech Republic + country_code: '420' + sms_only: false +DK: + name: Denmark + country_code: '45' + sms_only: false +DJ: + name: Djibouti + country_code: '253' + sms_only: true +TL: + name: East Timor + country_code: '670' + sms_only: true +EC: + name: Ecuador + country_code: '593' + sms_only: true +EG: + name: Egypt + country_code: '20' + sms_only: true +SV: + name: El Salvador + country_code: '503' + sms_only: true +GQ: + name: Equatorial Guinea + country_code: '240' + sms_only: true +ER: + name: Eritrea + country_code: '291' + sms_only: true +EE: + name: Estonia + country_code: '372' + sms_only: false +ET: + name: Ethiopia + country_code: '251' + sms_only: true +FK: + name: Falkland Islands + country_code: '500' + sms_only: true +FO: + name: Faroe Islands + country_code: '298' + sms_only: true +FJ: + name: Fiji + country_code: '679' + sms_only: true +FI: + name: Finland/Aland Islands + country_code: '358' + sms_only: false +FR: + name: France + country_code: '33' + sms_only: false +GF: + name: French Guiana + country_code: '594' + sms_only: true +PF: + name: French Polynesia + country_code: '689' + sms_only: true +GA: + name: Gabon + country_code: '241' + sms_only: true +GM: + name: Gambia + country_code: '220' + sms_only: true +GE: + name: Georgia + country_code: '995' + sms_only: true +DE: + name: Germany + country_code: '49' + sms_only: false +GH: + name: Ghana + country_code: '233' + sms_only: true +GI: + name: Gibraltar + country_code: '350' + sms_only: true +GR: + name: Greece + country_code: '30' + sms_only: true +GL: + name: Greenland + country_code: '299' + sms_only: true +GP: + name: Guadeloupe + country_code: '590' + sms_only: true +GT: + name: Guatemala + country_code: '502' + sms_only: true +GN: + name: Guinea + country_code: '224' + sms_only: true +GW: + name: Guinea-Bissau + country_code: '245' + sms_only: true +GY: + name: Guyana + country_code: '592' + sms_only: true +HT: + name: Haiti + country_code: '509' + sms_only: true +HN: + name: Honduras + country_code: '504' + sms_only: true +HK: + name: Hong Kong + country_code: '852' + sms_only: false +HU: + name: Hungary + country_code: '36' + sms_only: true +IS: + name: Iceland + country_code: '354' + sms_only: true +IN: + name: India + country_code: '91' + sms_only: false +ID: + name: Indonesia + country_code: '62' + sms_only: true +IR: + name: Iran + country_code: '98' + sms_only: true +IQ: + name: Iraq + country_code: '964' + sms_only: true +IE: + name: Ireland + country_code: '353' + sms_only: false +IL: + name: Israel + country_code: '972' + sms_only: false +IT: + name: Italy + country_code: '39' + sms_only: false +CI: + name: Ivory Coast + country_code: '225' + sms_only: true +JP: + name: Japan + country_code: '81' + sms_only: false +JO: + name: Jordan + country_code: '962' + sms_only: true +KZ: + name: Kazakhstan + country_code: '7' + sms_only: true +KE: + name: Kenya + country_code: '254' + sms_only: true +KP: + name: Korea (Democratic People's Republic of) + country_code: '850' + sms_only: true +KR: + name: Korea (Republic of) + country_code: '82' + sms_only: true +KW: + name: Kuwait + country_code: '965' + sms_only: true +KG: + name: Kyrgyzstan + country_code: '996' + sms_only: true +LA: + name: Laos People's Democratic Republic + country_code: '856' + sms_only: true +LV: + name: Latvia + country_code: '371' + sms_only: false +LB: + name: Lebanon + country_code: '961' + sms_only: true +LS: + name: Lesotho + country_code: '266' + sms_only: true +LR: + name: Liberia + country_code: '231' + sms_only: true +LY: + name: Libya + country_code: '218' + sms_only: true +LI: + name: Liechtenstein + country_code: '423' + sms_only: true +LT: + name: Lithuania + country_code: '370' + sms_only: false +LU: + name: Luxembourg + country_code: '352' + sms_only: false +MO: + name: Macau + country_code: '853' + sms_only: true +MK: + name: Macedonia + country_code: '389' + sms_only: true +MG: + name: Madagascar + country_code: '261' + sms_only: true +MW: + name: Malawi + country_code: '265' + sms_only: true +MY: + name: Malaysia + country_code: '60' + sms_only: true +MV: + name: Maldives + country_code: '960' + sms_only: true +ML: + name: Mali + country_code: '223' + sms_only: true +MT: + name: Malta + country_code: '356' + sms_only: false +MH: + name: Marshall Islands + country_code: '692' + sms_only: true +MQ: + name: Martinique + country_code: '596' + sms_only: true +MR: + name: Mauritania + country_code: '222' + sms_only: true +MU: + name: Mauritius + country_code: '230' + sms_only: true +YT: + name: Mayotte + country_code: '262' + sms_only: true +MX: + name: Mexico + country_code: '52' + sms_only: false +FM: + name: Micronesia + country_code: '691' + sms_only: true +MD: + name: Moldo + country_code: '373' + sms_only: true +MC: + name: Monaco + country_code: '377' + sms_only: true +MN: + name: Mongolia + country_code: '976' + sms_only: true +ME: + name: Montenegro + country_code: '382' + sms_only: true +MA: + name: Morocco + country_code: '212' + sms_only: true +MZ: + name: Mozambique + country_code: '258' + sms_only: true +MM: + name: Myanmar + country_code: '95' + sms_only: true +NA: + name: Namibia + country_code: '264' + sms_only: true +NP: + name: Nepal + country_code: '977' + sms_only: true +NL: + name: Netherlands + country_code: '31' + sms_only: false +BQ: + name: Netherlands Antilles + country_code: '599' + sms_only: true +NC: + name: New Caledonia + country_code: '687' + sms_only: true +NZ: + name: New Zealand + country_code: '64' + sms_only: false +NI: + name: Nicaragua + country_code: '505' + sms_only: true +NE: + name: Niger + country_code: '227' + sms_only: true +NG: + name: Nigeria + country_code: '234' + sms_only: true +NO: + name: Norway + country_code: '47' + sms_only: false +OM: + name: Oman + country_code: '968' + sms_only: true +PK: + name: Pakistan + country_code: '92' + sms_only: true +PW: + name: Palau + country_code: '680' + sms_only: true +PS: + name: Palestine + country_code: '970' + sms_only: true +PA: + name: Panama + country_code: '507' + sms_only: true +PG: + name: Papua New Guinea + country_code: '675' + sms_only: true +PY: + name: Paraguay + country_code: '595' + sms_only: true +PE: + name: Peru + country_code: '51' + sms_only: false +PH: + name: Philippines + country_code: '63' + sms_only: true +PL: + name: Poland + country_code: '48' + sms_only: false +PT: + name: Portugal + country_code: '351' + sms_only: true +QA: + name: Qatar + country_code: '974' + sms_only: true +RO: + name: Romania + country_code: '40' + sms_only: false +RU: + name: Russia + country_code: '7' + sms_only: true +RW: + name: Rwanda + country_code: '250' + sms_only: true +RE: + name: Reunion + country_code: '262' + sms_only: true +PM: + name: Saint Pierre and Miquelon + country_code: '508' + sms_only: true +WS: + name: Samoa + country_code: '685' + sms_only: true +SM: + name: San Marino + country_code: '378' + sms_only: true +ST: + name: Sao Tome and Principe + country_code: '239' + sms_only: true +SA: + name: Saudi Arabia + country_code: '966' + sms_only: true +SN: + name: Senegal + country_code: '221' + sms_only: true +RS: + name: Serbia + country_code: '381' + sms_only: true +SC: + name: Seychelles + country_code: '248' + sms_only: true +SL: + name: Sierra Leone + country_code: '232' + sms_only: true +SG: + name: Singapore + country_code: '65' + sms_only: true +SK: + name: Slovakia + country_code: '421' + sms_only: false +SI: + name: Slovenia + country_code: '386' + sms_only: true +SB: + name: Solomon Islands + country_code: '677' + sms_only: true +SO: + name: Somalia + country_code: '252' + sms_only: true +ZA: + name: South Africa + country_code: '27' + sms_only: false +SS: + name: South Sudan + country_code: '211' + sms_only: true +ES: + name: Spain + country_code: '34' + sms_only: false +LK: + name: Sri Lanka + country_code: '94' + sms_only: true +SD: + name: Sudan + country_code: '249' + sms_only: true +SR: + name: Suriname + country_code: '597' + sms_only: true +SZ: + name: Swaziland + country_code: '268' + sms_only: true +SE: + name: Sweden + country_code: '46' + sms_only: true +CH: + name: Switzerland + country_code: '41' + sms_only: false +SY: + name: Syria + country_code: '963' + sms_only: true +TW: + name: Taiwan + country_code: '886' + sms_only: true +TJ: + name: Tajikistan + country_code: '992' + sms_only: true +TZ: + name: Tanzania, United Republic of + country_code: '255' + sms_only: true +TH: + name: Thailand + country_code: '66' + sms_only: true +TG: + name: Togo + country_code: '228' + sms_only: true +TO: + name: Tonga + country_code: '676' + sms_only: true +TN: + name: Tunisia + country_code: '216' + sms_only: true +TR: + name: Turkey + country_code: '90' + sms_only: true +TM: + name: Turkmenistan + country_code: '993' + sms_only: true +TV: + name: Tuvalu + country_code: '688' + sms_only: true +UG: + name: Uganda + country_code: '256' + sms_only: true +UA: + name: Ukraine + country_code: '380' + sms_only: true +AE: + name: United Arab Emirates + country_code: '971' + sms_only: true +GB: + name: United Kingdom + country_code: '44' + sms_only: false +UY: + name: Uruguay + country_code: '598' + sms_only: true +UZ: + name: Uzbekistan + country_code: '998' + sms_only: true +VU: + name: Vanuatu + country_code: '678' + sms_only: true +VE: + name: Venezuela + country_code: '58' + sms_only: true +VN: + name: Vietnam + country_code: '84' + sms_only: true +VG: + name: Virgin Islands (British) + country_code: '1' + sms_only: true +VI: + name: Virgin Islands (U.S.) + country_code: '1' + sms_only: true +YE: + name: Yemen + country_code: '967' + sms_only: true +ZM: + name: Zambia + country_code: '260' + sms_only: true +ZW: + name: Zimbabwe + country_code: '263' + sms_only: true diff --git a/config/environments/production.rb b/config/environments/production.rb index b16dad0505f..db18fb995e8 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -10,7 +10,6 @@ config.i18n.fallbacks = true config.active_support.deprecation = :notify config.active_record.dump_schema_after_migration = false - config.action_mailer.smtp_settings = JSON.parse(Figaro.env.smtp_settings).symbolize_keys config.action_mailer.default_url_options = { host: Figaro.env.domain_name, @@ -19,7 +18,11 @@ config.action_mailer.asset_host = Figaro.env.mailer_domain_name config.action_mailer.raise_delivery_errors = false config.action_mailer.default_options = { from: Figaro.env.email_from } - config.action_mailer.delivery_method = :test if Figaro.env.disable_email_sending == 'true' + config.action_mailer.delivery_method = if Figaro.env.disable_email_sending == 'true' + :test + else + :mandrill + end routes.default_url_options[:protocol] = :https diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index c17e130b72d..0a6637d7c31 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -101,7 +101,7 @@ ignore_unused: - 'links.remove' - 'valid_email.validations.email.invalid' - 'zxcvbn.*' - - 'instructions.2fa.{sms,voice}.fallback_html' + - 'instructions.mfa.{sms,voice}.fallback_html' - 'links.phone_confirmation.fallback_to_{sms,authenticator,voice}_html' - 'links.two_factor_authentication.resend_code.{sms,voice}_html' - 'links.two_factor_authentication.{sms,voice}_html' diff --git a/config/initializers/active_job_logger_patch.rb b/config/initializers/active_job_logger_patch.rb index 9d3dcc07960..7739a1d86b5 100644 --- a/config/initializers/active_job_logger_patch.rb +++ b/config/initializers/active_job_logger_patch.rb @@ -5,8 +5,33 @@ module ActiveJob module Logging class LogSubscriber + def enqueue(event) + info { json_for(event: event, event_type: 'Enqueued') } + end + + def perform_start(event) + info { json_for(event: event, event_type: 'Performing') } + end + + def perform(event) + info { json_for(event: event, event_type: 'Performed') } + end + private + def json_for(event:, event_type:) + job = event.payload[:job] + + { + timestamp: Time.zone.now, + event_type: event_type, + job_class: job.class.name, + job_queue: queue_name(event), + job_id: job.job_id, + duration: "#{event.duration.round(2)}ms", + }.to_json + end + def args_info(_job) '' end diff --git a/config/initializers/mandrill.rb b/config/initializers/mandrill.rb new file mode 100644 index 00000000000..47705b72f19 --- /dev/null +++ b/config/initializers/mandrill.rb @@ -0,0 +1,5 @@ +if Figaro.env.mandrill_api_token.present? + MandrillDm.configure do |config| + config.api_key = Figaro.env.mandrill_api_token + end +end diff --git a/config/locales/account/en.yml b/config/locales/account/en.yml index 598b0a31909..bb873acc6e1 100644 --- a/config/locales/account/en.yml +++ b/config/locales/account/en.yml @@ -3,9 +3,9 @@ en: account: index: address: Current address - authentication_app: Authentication app - auth_app_enabled: enabled auth_app_disabled: not enabled + auth_app_enabled: enabled + authentication_app: Authentication app dob: Date of birth email: Email address full_name: Full name diff --git a/config/locales/account/es.yml b/config/locales/account/es.yml index 27f9b56a511..3c06f750c71 100644 --- a/config/locales/account/es.yml +++ b/config/locales/account/es.yml @@ -2,31 +2,31 @@ es: account: index: - address: NOT TRANSLATED YET - authentication_app: NOT TRANSLATED YET - auth_app_enabled: NOT TRANSLATED YET - auth_app_disabled: NOT TRANSLATED YET - dob: NOT TRANSLATED YET - email: NOT TRANSLATED YET - full_name: NOT TRANSLATED YET - login: NOT TRANSLATED YET - password: NOT TRANSLATED YET - phone: NOT TRANSLATED YET - previous_address: NOT TRANSLATED YET + address: Dirección actual + auth_app_disabled: no permitido + auth_app_enabled: permitido + authentication_app: App de autenticación + dob: Fecha de nacimiento + email: Email + full_name: Nombre completo + login: Información para iniciar sesión + password: Contraseña + phone: Teléfono + previous_address: Dirección anterior reactivation: - instructions: NOT TRANSLATED YET - link: NOT TRANSLATED YET - ssn: NOT TRANSLATED YET + instructions: Su perfil ha sido desactivado debido a un cambio de contraseña. + link: Reactive su perfil ahora. + ssn: Número de Seguro Social verification: - instructions: NOT TRANSLATED YET - reactivate_button: NOT TRANSLATED YET - success: NOT TRANSLATED YET - with_phone_button: NOT TRANSLATED YET + instructions: Su cuenta requiere que un código secreto sea verificado. + reactivate_button: Ingrese el código que recibió por correo postal. + success: Su cuenta ha sido verificada. + with_phone_button: Verifique con su teléfono. items: - personal_key: NOT TRANSLATED YET + personal_key: Clave personal links: - regenerate_personal_key: NOT TRANSLATED YET + regenerate_personal_key: Obtenga una clave nueva. security: - link: NOT TRANSLATED YET - text: NOT TRANSLATED YET - welcome: Bienvenido + link: Obtenga más información en el Centro de ayuda + text: Para su seguridad, la información de su perfil está bloqueada. + welcome: Bienvenido/a diff --git a/config/locales/account/fr.yml b/config/locales/account/fr.yml new file mode 100644 index 00000000000..650827dd753 --- /dev/null +++ b/config/locales/account/fr.yml @@ -0,0 +1,34 @@ +--- +fr: + account: + index: + address: Adresse actuelle + auth_app_disabled: non activée + auth_app_enabled: activée + authentication_app: Application d'authentification + dob: Date de naissance + email: Adresse courriel + full_name: Nom complet + login: Information de connexion + password: Mot de passe + phone: Numéro de téléphone + previous_address: Adresse précédente + reactivation: + instructions: Votre profil a été récemment désactivé en raison d'une réinitialisation + de mot passe. Vous pouvez utiliser votre clé personnelle pour réactiver + votre profil. + link: NOT TRANSLATED YET + ssn: Numéro d'assurance sociale + verification: + instructions: Votre compte requiert la vérification d'un code secret. + reactivate_button: Entrez le code que vous avez reçu par la poste + success: Votre compte a été vérifié. + with_phone_button: Verifiez avec votre téléphone + items: + personal_key: Clé personnelle + links: + regenerate_personal_key: Obtenez une nouvelle clé + security: + link: En apprendre davantage dans le Centre d'aide + text: L'information de votre profil est verrouillée pour votre sécurité. + welcome: Bienvenue diff --git a/config/locales/activerecord/en.yml b/config/locales/activerecord/en.yml index 5bffa82f950..5c8acff29cc 100644 --- a/config/locales/activerecord/en.yml +++ b/config/locales/activerecord/en.yml @@ -1,3 +1,4 @@ +--- en: activerecord: errors: @@ -5,5 +6,6 @@ en: app_setting: attributes: value: - cannot_disable_2fa_in_prod: Two-factor authentication cannot be disabled in production + cannot_disable_2fa_in_prod: Two-factor authentication cannot be disabled + in production invalid: Value must be '1' or '0' diff --git a/config/locales/activerecord/es.yml b/config/locales/activerecord/es.yml index d4dc5ed34fb..8012f7b7422 100644 --- a/config/locales/activerecord/es.yml +++ b/config/locales/activerecord/es.yml @@ -1,3 +1,4 @@ +--- es: activerecord: errors: @@ -5,5 +6,6 @@ es: app_setting: attributes: value: - cannot_disable_2fa_in_prod: La autenticación de dos factores no se puede desactivar en la producción - invalid: El valor tiene que ser '1' o '0' + cannot_disable_2fa_in_prod: La autenticación de dos factores no se puede + suspender en producción + invalid: El valor debe ser '1' o '0' diff --git a/config/locales/activerecord/fr.yml b/config/locales/activerecord/fr.yml new file mode 100644 index 00000000000..86bdb29d16f --- /dev/null +++ b/config/locales/activerecord/fr.yml @@ -0,0 +1,11 @@ +--- +fr: + activerecord: + errors: + models: + app_setting: + attributes: + value: + cannot_disable_2fa_in_prod: L'authentification à deux facteurs ne peut + être désactivée en production + invalid: La valeur doit être de '1' ou de '0' diff --git a/config/locales/devise/en.yml b/config/locales/devise/en.yml index 94019d25c3f..967cb8e345a 100644 --- a/config/locales/devise/en.yml +++ b/config/locales/devise/en.yml @@ -5,12 +5,10 @@ en: already_confirmed: Your email address has already been confirmed. %{action} confirmed: You have confirmed your email address confirmed_but_must_set_password: You have confirmed your email address - send_instructions: > - You will receive an email with instructions for how to confirm your - email address in a few minutes. - send_paranoid_instructions: > - You will receive an email with instructions for how to confirm your email - address in a few minutes. + send_instructions: You will receive an email with instructions for how to confirm + your email address in a few minutes. + send_paranoid_instructions: You will receive an email with instructions for + how to confirm your email address in a few minutes. failure: already_authenticated: '' inactive: Your account is not activated yet. @@ -18,9 +16,8 @@ en: last_attempt: You have one more attempt before your account is locked. locked: Your account is now locked. not_found_in_database: Invalid email or password. - session_limited: > - Your login credentials were used in another browser. Please sign in - again to continue in this browser. + session_limited: Your login credentials were used in another browser. Please + sign in again to continue in this browser. timeout: Your session expired. Please sign in again to continue. unauthenticated: '' unconfirmed: You need to confirm your email address before continuing. @@ -36,65 +33,55 @@ en: passwords: choose_new_password: Choose a new password. invalid_token: The reset password token is invalid. Try again. - no_token: > - You can’t access this page without coming from a password reset email. - If you do come from a password reset email, please make sure you used the full - link provided. - send_instructions: > - You will receive an email with instructions on how to reset your - password in a few minutes. - send_paranoid_instructions: > - You will receive an email with instructions on how to reset your - password in a few minutes. + no_token: You can’t access this page without coming from a password reset email. + If you do come from a password reset email, please make sure you used the + full link provided. + send_instructions: You will receive an email with instructions on how to reset + your password in a few minutes. + send_paranoid_instructions: You will receive an email with instructions on how + to reset your password in a few minutes. token_expired: You took too long to reset your password. Try again. updated: Your password has been changed. You are now signed in. - updated_not_active: > - Your password has been changed. Please sign in with your new password. + updated_not_active: Your password has been changed. Please sign in with your + new password. registrations: close_window: You can close this window if you are done. - destroy_confirm: > - Deleting your account cannot be undone. All data associated with your - account will be removed. Are you sure you’d like to delete your account? + destroy_confirm: Deleting your account cannot be undone. All data associated + with your account will be removed. Are you sure you’d like to delete your + account? destroyed: Your account has been successfully deleted. - email_and_phone_need_confirmation: > - Before we finish updating your account, we need to confirm both your new - number and new email. Please follow the instructions below to confirm your new - number, then check for an email from us. Follow the link in the email to confirm - your new email address. - email_update_needs_confirmation: > - You updated your account, but we need to confirm your new email address. - Check for an email from us, then follow the link in the email to confirm your - new address. + email_and_phone_need_confirmation: Before we finish updating your account, we + need to confirm both your new number and new email. Please follow the instructions + below to confirm your new number, then check for an email from us. Follow + the link in the email to confirm your new email address. + email_update_needs_confirmation: You updated your account, but we need to confirm + your new email address. Check for an email from us, then follow the link in + the email to confirm your new address. enabled_twofactor: You have enabled two-factor authentication. - phone_update_needs_confirmation: > - Your request to update your phone number was processed, but we need to - confirm your new number first. Please follow the instructions below. If you do - not confirm your new number, we will keep using your old phone number. + phone_update_needs_confirmation: Your request to update your phone number was + processed, but we need to confirm your new number first. Please follow the + instructions below. If you do not confirm your new number, we will keep using + your old phone number. signed_up: Welcome! You have created an account. - signed_up_but_inactive: > - You have created an account. However, we could not sign you in because - your account is not yet activated. - signed_up_but_locked: > - You have created an account. However, we could not sign you in because - your account is locked. + signed_up_but_inactive: You have created an account. However, we could not sign + you in because your account is not yet activated. + signed_up_but_locked: You have created an account. However, we could not sign + you in because your account is locked. start: accordion: You will need - bullet_1_html: Provide your email address and create a - strong password. - bullet_2_html: > - Enable two-step authentication. This adds a second layer of security - to your account by requiring you to enter a new code that’s sent to your phone every time - you log in. - bullet_3_html: > - Provide basic information, such as your name, address, phone number, - and social security number. - bullet_4_html: > - Provide a financial account number, such as your credit card number. - This number will only be used to verify your identity. - bullet_5_html: > - When you are finished with this process we’ll give you a personal key. Write - it down and store it in a safe place; it’s important. You’ll be asked for the - personal key every time you make changes to your account. + bullet_1_html: Provide your email address and create a strong + password. + bullet_2_html: Enable two-step authentication. This adds + a second layer of security to your account by requiring you to enter a new + code that’s sent to your phone every time you log in. + bullet_3_html: Provide basic information, such as your name, + address, phone number, and social security number. + bullet_4_html: Provide a financial account number, such as + your credit card number. This number will only be used to verify your identity. + bullet_5_html: When you are finished with this process we’ll give you a personal + key. Write it down and store it in a safe place; it’s important. + You’ll be asked for the personal key every time you make changes to your + account. learn_more: Learn more about verifying your identity updated: Your account has been updated! sessions: @@ -105,52 +92,48 @@ en: buttons: confirm_with_sms: Confirm with text message confirm_with_voice: Confirm with voice call - choose_delivery_confirmation: > - How would you like to receive your confirmation security code for %{phone}? - choose_otp_delivery_html: > - We will send it to %{phone} immediately. Message and data rates may apply. + choose_delivery_confirmation: How would you like to receive your confirmation + security code for %{phone}? + choose_otp_delivery_html: We will send it to %{phone} immediately. Message and + data rates may apply. contact_administrator: Please contact your system administrator. header_text: Enter your security code - invalid_otp: > - That security code is invalid. You can try entering it again or request a new - one-time security code. + invalid_otp: That security code is invalid. You can try entering it again or + request a new one-time security code. invalid_personal_key: That personal key is invalid. - max_generic_login_attempts_reached: > - Your account is temporarily locked. - max_otp_login_attempts_reached: > - Your account is temporarily locked because you have entered the one-time - security code incorrectly too many times. - max_otp_requests_reached: > - Your account is temporarily locked because you have requested a - security code too many times. - max_personal_key_login_attempts_reached: > - Your account is temporarily locked because you have entered the personal key - incorrectly too many times. + max_generic_login_attempts_reached: Your account is temporarily locked. + max_otp_login_attempts_reached: Your account is temporarily locked because you + have entered the one-time security code incorrectly too many times. + max_otp_requests_reached: Your account is temporarily locked because you have + requested a security code too many times. + max_personal_key_login_attempts_reached: Your account is temporarily locked + because you have entered the personal key incorrectly too many times. otp_delivery_preference: instruction: You can change your choice the next time you sign in + phone_unsupported: We're unable to make phone calls to people in %{location} + at this time. You will receive your security code via text message sms: Text message (SMS) title: How would you like to receive your security code? voice: Phone call otp_phone_label: Phone number otp_phone_label_info: Mobile or landline okay - otp_setup_html: > - Every time you log in, we will send you a one-time - security code via text message or phone call. This helps safeguard your account. + otp_phone_label_info_modile_only: Mobile only + otp_setup_html: "Every time you log in, we will send you a + one-time security code via text message or phone call. This helps safeguard + your account." otp_sms_disclaimer: Message and data rates may apply. - please_confirm: > - Your phone number has been set. Confirm it by entering the security code below. - please_try_again_html: Please try again in %{time_remaining}. personal_key_fallback: link: Use a personal key instead text_html: Don’t have access to your phone? %{link}. personal_key_header_text: Enter your personal key - personal_key_prompt: > - You can use this personal key once. If you still need a code after - signing in, go to your account settings page to get a new one. + personal_key_prompt: You can use this personal key once. If you still need a + code after signing in, go to your account settings page to get a new one. + please_confirm: Your phone number has been set. Confirm it by entering the security + code below. + please_try_again_html: Please try again in %{time_remaining}. totp_fallback: sms_link_text: get a code via text message - text_html: > - If you can’t use your authenticator app right now you can %{sms_link} + text_html: If you can’t use your authenticator app right now you can %{sms_link} or %{voice_link}. voice_link_text: get a code with a phone call totp_header_text: Enter your authentication app code diff --git a/config/locales/devise/es.yml b/config/locales/devise/es.yml index acbd5a27c26..03801a1189a 100644 --- a/config/locales/devise/es.yml +++ b/config/locales/devise/es.yml @@ -1,28 +1,26 @@ +--- es: devise: confirmations: - already_confirmed: Su dirección de correo electrónico ya ha sido confirmada. %{acción} - confirmed: Su dirección de correo electrónico esta confirmada. - confirmed_but_must_set_password: Ha confirmado tu dirección de correo electrónico - send_instructions: - Recibirá un correo electrónico con instrucciones para confirmar su - dirección de correo electrónico en breve. - send_paranoid_instructions: - Recibirá un correo electrónico con instrucciones para confirmar su - dirección de correo electrónico en breve. + already_confirmed: Su email ha sido confirmado. %{acción} + confirmed: Usted ha confirmado su email. + confirmed_but_must_set_password: Usted ha confirmado su email. + send_instructions: En pocos minutos recibirá un email con instrucciones para + confirmar el uso de su email. + send_paranoid_instructions: En pocos minutos recibirá un email con instrucciones + para confirmar el uso de su email. failure: already_authenticated: '' inactive: Su cuenta aún no está activada. - invalid: Dirección de correo electrónico o contraseña no válidos. + invalid: Email o contraseña no válido. last_attempt: Tiene un intento más antes de que su cuenta esté bloqueada. locked: Su cuenta está bloqueada. - not_found_in_database: Dirección de correo electrónico o contraseña no válidos. - session_limited: - Sus credenciales de inicio de sesión se utilizaron en otro navegador. Inicia sesión - de nuevo para continuar en este navegador. - timeout: Su sesión ha caducado. Vuelva a iniciar sesión para continuar. + not_found_in_database: Email o contraseña no válido. + session_limited: Sus credenciales para iniciar una sesión se utilizaron en otro + navegador. Inicie una sesión nueva para continuar en este navegador. + timeout: Su sesión ha caducado. Vuelva a iniciar la sesión para continuar. unauthenticated: '' - unconfirmed: Necesita confirmar su dirección de correo electrónico antes de continuar. + unconfirmed: Debe confirmar su email antes de continuar. mailer: account_locked: subject: Su cuenta de login.gov ha sido bloqueada. @@ -31,116 +29,122 @@ es: password_updated: subject: Su contraseña ha sido cambiada. reset_password_instructions: - subject: Restablecer su contraseña. + subject: Restablezca su contraseña. passwords: choose_new_password: Elija una contraseña nueva. - invalid_token: El token de contraseña de restablecimiento no es válido. Inténtalo de nuevo. - no_token: - No puede acceder a esta página sin proceder de un correo electrónico de restablecimiento de contraseña. - Si procede de un correo electrónico de restablecimiento de contraseña, asegúrese de utilizar el - enlace proporcionado. - send_instructions: - Recibirá un correo electrónico con instrucciones sobre cómo restablecer su - contraseña en pocos minutos. - send_paranoid_instructions: - Recibirá un correo electrónico con instrucciones sobre cómo restablecer su - contraseña en pocos minutos. - token_expired: Ha tardado demasiado en restablecer su contraseña. Inténtalo de nuevo. - updated: Su contraseña ha sido cambiada. Ahora ha iniciado sesión. - updated_not_active: - Su contraseña ha sido cambiada. Inicia sesión con su nueva contraseña. + invalid_token: El código para restablecer la contraseña no es válido. Inténtelo + de nuevo. + no_token: Usted no puede acceder a esta página si no procede desde el email + para restablecer la contraseña. Asegúrese de utilizar el enlace completo que + está en el email. + send_instructions: En pocos minutos recibirá un email con instrucciones para + restablecer su contraseña. + send_paranoid_instructions: En pocos minutos recibirá un email con instrucciones + para restablecer su contraseña. + token_expired: Ha tardado demasiado en restablecer su contraseña. Inténtelo + de nuevo. + updated: Su contraseña ha sido cambiada. Ahora ha iniciado una sesión. + updated_not_active: Su contraseña ha sido cambiada. Inicie sesión con su contraseña + nueva. registrations: - close_window: Puede cerrar esta ventana del navegador una vez que haya confirmado su dirección de correo electrónico. - destroy_confirm: - No se puede deshacer la eliminación de su cuenta. Todos los datos asociados a su - suenta se eliminará. ¿Seguro que desea eliminar su cuenta? + close_window: Puede cerrar esta ventana del navegador una vez que haya finalizado. + destroy_confirm: No se puede deshacer la eliminación de su cuenta. Todos los + datos asociados a su cuenta se eliminarán. ¿Confirma que desea eliminar su + cuenta? destroyed: Su cuenta se ha eliminado exitosamente. - email_and_phone_need_confirmation: - Antes de terminar de actualizar su cuenta, debemos confirmar su nuevo - número y el nuevo correo electrónico. Siga las instrucciones a continuación para confirmar su nuevo - número y, a continuación, compruebe si hay un correo electrónico de nosotros. Siga el enlace en el correo electrónico para confirmar su - nueva dirección de correo electrónico. - email_update_needs_confirmation: - Has actualizado su cuenta, pero debemos confirmar su nueva dirección de correo electrónico. - Busque un correo electrónico de nosotros, luego siga el enlace en el correo electrónico para confirmar su - nueva dirección. + email_and_phone_need_confirmation: Antes de terminar de actualizar su cuenta, + debemos confirmar su nuevo número y nuevo email. Siga las siguientes instrucciones + para confirmar su nuevo número y, luego, encuentre el email que le enviamos + nosotros. Siga el enlace en el email para confirmar su nueva dirección de + email. + email_update_needs_confirmation: Ha actualizado su cuenta, pero debemos confirmar + su nueva dirección de email. Encuentre el email que nosotros le enviamos y + siga el enlace para confirmar su nueva dirección. enabled_twofactor: Ha habilitado la autenticación de dos factores. - phone_update_needs_confirmation: - Su solicitud para actualizar su número de teléfono fue procesada, pero necesitamos - confirmar primero su número nuevo. Por favor, siga las siguientes instrucciones. Si no - confirma su nuevo número, seguiremos usando su número de teléfono antiguo. - signed_up: ¡Bienvenido! Usted ha creado una cuenta. - signed_up_but_inactive: - Ha creado una cuenta. Sin embargo, no hemos podido iniciar sesión porque - su cuenta aún no está activada. - signed_up_but_locked: - Ha creado una cuenta. Sin embargo, no hemos podido iniciar sesión porque - su cuenta está bloqueada. + phone_update_needs_confirmation: Su solicitud para actualizar su número de teléfono + fue procesada, pero necesitamos confirmar primero su número nuevo. Por favor, + siga las siguientes instrucciones. Si no confirma su nuevo número, seguiremos + usando su número de teléfono antiguo. + signed_up: "¡Bienvenido/a! Usted ha creado una cuenta." + signed_up_but_inactive: Usted ha creado una cuenta. No hemos podido iniciar + su sesión porque su cuenta aún no está activada. + signed_up_but_locked: Usted ha creado una cuenta. No hemos podido iniciar su + sesión porque su cuenta está bloqueada. start: - accordion: NOT TRANSLATED YET - bullet_1_html: NOT TRANSLATED YET - bullet_2_html: NOT TRANSLATED YET - bullet_3_html: NOT TRANSLATED YET - bullet_4_html: NOT TRANSLATED YET - bullet_5_html: NOT TRANSLATED YET - learn_more: NOT TRANSLATED YET - updated: ¡Su cuenta ha sido actualizada! + accordion: Usted necesitará + bullet_1_html: Proporcione su dirección de email y establezca + una contraseña segura. + bullet_2_html: Permita la autenticación de dos factores. + Esto agrega una segunda capa de seguridad a su cuenta al requerirle que + ingrese un nuevo código que recibirá en su teléfono cada vez que inicie + una sesión. + bullet_3_html: Proporcione información básica como su nombre, + dirección, número de teléfono y número de Seguro Social. + bullet_4_html: Proporcione un número de cuenta financiera + como su número de tarjeta de crédito. Este número solamente será usado para + verificar su identidad. + bullet_5_html: Cuando haya terminado con este proceso, le daremos una clave + personal. Escríbala y guárdela en un lugar seguro; es importante. + Se le pedirá la clave personal cada vez que realice cambios en su cuenta. + learn_more: Obtenga más información sobre la verificación de su identidad. + updated: "¡Su cuenta ha sido actualizada!" sessions: - already_signed_out: Ahora se ha cerrado la sesión. + already_signed_out: Su sesión ha terminado ahora. signed_in: '' - signed_out: Ahora se ha cerrado la sesión. + signed_out: Su sesión ha terminado ahora. two_factor_authentication: buttons: confirm_with_sms: Confirmar con mensaje de texto confirm_with_voice: Confirmar con llamada de voz - choose_delivery_confirmation: - ¿Cómo le gustaría recibir su código de confirmación para %{phone}? - choose_otp_delivery_html: - Selecciona cómo deseas recibir su código de acceso único para %{phone}. - contact_administrator: Por favor, póngase en contacto con el administrador del sistema. - header_text: Ingrese su contraseña. - invalid_otp: - Esa contraseña no es válida. Puede intentar volver a ingresarlo o solicitar una nueva - Código de acceso único. - invalid_personal_key: Ese código de recuperación no es válido. - max_generic_login_attempts_reached: - Su cuenta ha sido bloqueada temporalmente. - max_otp_login_attempts_reached: - Su cuenta ha sido bloqueada temporalmente porque ha ingresado el código de acceso único - de forma incorrecta demasiadas veces. - max_otp_requests_reached: NOT TRANSLATED YET - max_personal_key_login_attempts_reached: - Su cuenta ha sido bloqueada temporalmente porque ha ingresado el código de acceso único - de forma incorrecta demasiadas veces. + choose_delivery_confirmation: "¿Cómo le gustaría recibir su código de confirmación + para %{phone}?" + choose_otp_delivery_html: Lo enviaremos a %{phone} inmediatamente. Puede estar + sujeto a cargos de mensajería y datos. + contact_administrator: Por favor, póngase en contacto con el administrador del + sistema. + header_text: Ingrese su código de seguridad + invalid_otp: Ese código de seguridad no es válido. Puede intentar ingresarlo + de nuevo o solicitar un nuevo código de seguridad de sólo un uso. + invalid_personal_key: Esa clave personal no es válida. + max_generic_login_attempts_reached: Su cuenta está bloqueada temporalmente. + max_otp_login_attempts_reached: Su cuenta ha sido bloqueada temporalmente porque + ha ingresado incorrectamente el código de seguridad de sólo un uso demasiadas + veces. + max_otp_requests_reached: Su cuenta ha sido bloqueada temporalmente porque ha + solicitado un código de seguridad demasiadas veces más de lo permitido. + max_personal_key_login_attempts_reached: Su cuenta ha sido bloqueada temporalmente + porque ha ingresado incorrectamente la clave personal demasiadas veces. otp_delivery_preference: - instruction: Puede cambiar su elección la próxima vez que inicie sesión. - sms: Mensaje de texto (SMS) - title: ¿Cómo le gustaría recibir su código de acceso? + instruction: Puede cambiar su elección la próxima vez que inicie una sesión. + phone_unsupported: NOT TRANSLATED YET + sms: Mensaje de texto (SMS, sigla en inglés) + title: "¿Cómo le gustaría recibir su código de seguridad?" voice: Llamada telefónica otp_phone_label: Número de teléfono - otp_phone_label_info: Móvil o teléfono fijo está bien - otp_setup_html: - Cada vez que inicies sesión, le enviaremos un código de acceso único - a través de un mensaje de texto o una llamada telefónica. Esto ayuda a proteger su cuenta. - otp_sms_disclaimer: Se pueden aplicar tarifas de mensajes y datos. - please_confirm: - Se ha establecido el número de teléfono. Confírmelo introduciendo el código de acceso siguiente. - please_try_again_html: Inténtalo de nuevo en %{time_remaining}. + otp_phone_label_info: El móvil o teléfono fijo está bien. + otp_phone_label_info_modile_only: NOT TRANSLATED YET + otp_setup_html: "Cada vez que inicie una sesión, le enviaremos + un código de seguridad de sólo un uso por mensaje de texto o llamada telefónica. + Esto ayuda a proteger su cuenta." + otp_sms_disclaimer: Puede estar sujeto a cargos de mensajería y datos. personal_key_fallback: - link: código de recuperación - text_html: ¿No tiene acceso a su teléfono? Use el %{link} en su lugar. - personal_key_header_text: Ingrese su código de recuperación. - personal_key_prompt: > - Puede usar este código de recuperación una vez. Si todavía necesita un código después de - iniciar sesión, vaya a la página de configuración de su cuenta para obtener una nueva. + link: Use una clave personal en su lugar + text_html: "¿No tiene acceso a su teléfono? %{link}." + personal_key_header_text: Ingrese su clave personal + personal_key_prompt: Puede usar esta clave personal una vez. Si todavía necesita + un código después de iniciar una sesión, vaya a la página de configuración + de su cuenta para obtener una clave nueva. + please_confirm: Su número de teléfono ha sido establecido. Confírmelo ingresando + el código de seguridad a continuación. + please_try_again_html: Inténtelo de nuevo en %{time_remaining}. totp_fallback: sms_link_text: Recibir un código por mensaje de texto - text_html: > - Si no puede usar su aplicación de autenticador ahora, puede %{sms_link} - O %{voice_link}. - voice_link_text: con una llamada telefónica - totp_header_text: Ingrese su código de autenticador - totp_info: Use cualquier aplicación de autenticador para escanear el código de QR que aparece a continuación. - two_factor_setup: Agregar un número de teléfono + text_html: Si no puede usar su app de autenticación ahora, puede %{sms_link} + o %{voice_link}. + voice_link_text: obtenga un código con una llamada telefónica + totp_header_text: Ingrese su código de la app de autenticación + totp_info: Use cualquier app de autenticación para escanear el código QR que + aparece a continuación. + two_factor_setup: Añada un número de teléfono user: - new_otp_sent: Le enviaremos un nuevo código de acceso único. + new_otp_sent: Le enviamos un nuevo código de sólo un uso diff --git a/config/locales/devise/fr.yml b/config/locales/devise/fr.yml new file mode 100644 index 00000000000..983a01465bd --- /dev/null +++ b/config/locales/devise/fr.yml @@ -0,0 +1,155 @@ +--- +fr: + devise: + confirmations: + already_confirmed: Votre adresse courriel a déjà été confirmée. %{action} + confirmed: Vous avez confirmé votre adresse courriel + confirmed_but_must_set_password: Vous avez confirmé votre adresse courriel + send_instructions: Vous recevrez dans quelques instants un courriel avec des + instructions pour confirmer votre adresse courriel. + send_paranoid_instructions: Vous recevrez dans quelques instants un courriel + avec des instructions pour confirmer votre adresse courriel. + failure: + already_authenticated: '' + inactive: Votre compte n'est pas encore activé. + invalid: Adresse courriel ou mot de passe non valide. + last_attempt: Il vous reste un essai avant que votre compte ne soit verrouillé. + locked: Votre compte est maintenant verrouillé. + not_found_in_database: Adresse courriel ou mot de passe non valide. + session_limited: Vos authentifiants ont été utilisés dans un autre navigateur. + Veuillez vous connecter de nouveau pour continuer avec ce navigateur. + timeout: Votre session est expirée. Veuillez vous connecter de nouveau pour + continuer. + unauthenticated: '' + unconfirmed: Vous devez confirmer votre adresse courriel avant de continuer. + mailer: + account_locked: + subject: Votre compte login.gov a été verrouillé + confirmation_instructions: + subject: Confirmez votre adresse courriel + password_updated: + subject: Votre mot de passe a été modifié + reset_password_instructions: + subject: Réinitialisez votre mot de passe + passwords: + choose_new_password: Choisissez un nouveau mot de passe. + invalid_token: Le jeton de réinitialisation de mot de passe n'est pas valide. + Veuillez essayer de nouveau. + no_token: Vous ne pouvez accéder à cette page que depuis un courriel de réinitialisation + de mot de passe. Si vous avez été redirigé à partir d'un courriel de réinitialisation + de mot de passe, veuillez vous assurer que vous avez utilisé le lien fourni + complet. + send_instructions: Vous recevrez dans quelques instants un courriel avec des + instructions pour réinitialiser votre mot de passe. + send_paranoid_instructions: Vous recevrez dans quelques instants un courriel + avec des instructions pour réinitialiser votre mot de passe. + token_expired: Vous avez pris trop de temps pour réinitialiser votre mot de + passe. Veuillez essayer de nouveau. + updated: Votre mot de passe a été modifié. Vous êtes maintenant connectée(e). + updated_not_active: Votre mot de passe a été modifié. Veuillez vous connecter + avec votre nouveau mot de passe. + registrations: + close_window: Vous pouvez fermer cette fenêtre si vous avez terminé. + destroy_confirm: La suppression de votre compte est irréversible. Toutes les + données associées à votre compte seront effacées. Souhaitez-vous vraiment + supprimer votre compte? + destroyed: Votre compte a bien été supprimé. + email_and_phone_need_confirmation: Avant de terminer la mise-à-jour de votre + compte, nous devons confirmer votre nouveau numéro et votre nouvelle adresse + courriel. Veuillez suivre les instructions ci-dessous pour confirmer votre + nouveau numéro, puis surveillez la réception d'un courriel envoyé par login.gov. + Suivez le lien inclus dans le courriel pour confirmer votre nouvelle adresse + courriel. + email_update_needs_confirmation: Vous avez mis à jour votre compte, mais nous + devons confirmer votre nouvelle adresse courriel. Surveillez la réception + d'un courriel envoyé par nous et suivez le lien inscrit pour confirmer votre + nouvelle adresse courriel. + enabled_twofactor: Vous avez activé l'authentification à deux facteurs. + phone_update_needs_confirmation: Votre demande de mise à jour de votre numéro + de téléphone a été traitée, mais nous devons d'abord confirmer votre nouveau + numéro. Veuillez suivre les instructions ci-dessous. Si vous ne confirmez + pas votre nouveau numéro, nous continuerons d'utiliser votre ancien numéro + de téléphone. + signed_up: Bienvenue! Vous avez créé un compte. + signed_up_but_inactive: Vous avez créé un compte. Cependant, nous n'avons pu + vous connecter, car votre compte n'est pas encore activé. + signed_up_but_locked: Vous avez créé un compte. Cependant, nous n'avons pu vous + connecter, car votre compte est verrouillé. + start: + accordion: Vous devrez + bullet_1_html: fournir votre adresse courriel et créer un + mot de passe fort. + bullet_2_html: Activez l'authentification à deux facteurs. + Cela permet d'ajouter un degré de sécurité à votre compte, car vous devez + entrer un nouveau code envoyé à votre téléphone chaque fois que vous vous + connectez. + bullet_3_html: Fournissez de l'information de base, comme + votre nom, adresse et numéro de sécurité sociale. + bullet_4_html: Fournissez un numéro de compte bancaire, comme + votre numéro de carte de crédit. + bullet_5_html: 'Lorsque vous aurez terminé ce processus, nous vous donnerons + une clé personnelle. Notez-la et placez-la en lieu sûr : c''est + important. On vous demandera la clé personnelle chaque fois que + vous apporterez des changements à votre compte.' + learn_more: En savoir plus sur la vérification de votre identité + updated: Votre compte a été mis à jour! + sessions: + already_signed_out: Vous êtes maintenant connecté(e). + signed_in: '' + signed_out: Vous êtes maintenant déconnecté(e). + two_factor_authentication: + buttons: + confirm_with_sms: Confirmer par SMS + confirm_with_voice: Confirmer avec un appel vocal + choose_delivery_confirmation: Comment souhaitez-vous recevoir votre code de + sécurité de confirmation pour %{phone}? + choose_otp_delivery_html: Nous l'envoyons à %{phone} immédiatement. Les tarifs + liés aux SMS et aux données peuvent s'appliquer. + contact_administrator: Veuillez communiquer avec votre administrateur système. + header_text: Entrez votre code de sécurité + invalid_otp: Ce code de sécurité est non valide. Vous pouvez essayer de l'entrer + de nouveau ou demander un nouveau code de sécurité à utilisation unique. + invalid_personal_key: Cette clé personnelle est non valide. + max_generic_login_attempts_reached: Votre compte est temporairement verrouillé. + max_otp_login_attempts_reached: Votre compte est temporairement verrouillé, + car vous avez entré le code de sécurité à utilisation unique de façon erronée + à de trop nombreuses reprises. + max_otp_requests_reached: NOT TRANSLATED YET + max_personal_key_login_attempts_reached: Votre compte est temporairement verrouillé, + car vous avez entré le code de sécurité à utilisation unique de façon erronée + à de trop nombreuses reprises. + otp_delivery_preference: + instruction: Vous pourrez changer votre choix la prochaine fois que vous vous + connecterez + phone_unsupported: NOT TRANSLATED YET + sms: Message texte (SMS) + title: Comment souhaitez-vous recevoir votre code de sécurité? + voice: Appel téléphonique + otp_phone_label: Numéro de téléphone + otp_phone_label_info: Cellulaire ou ligne fixe est acceptable + otp_phone_label_info_modile_only: NOT TRANSLATED YET + otp_setup_html: "Chaque fois que vous vous connecterez, nous + vous enverrons un code de sécurité à utilisation unique par message texte + ou par appel téléphonique. Cela aide à protéger votre compte." + otp_sms_disclaimer: Les tarifs liés aux SMS et aux données peuvent s'appliquer. + personal_key_fallback: + link: Utlisez plutôt une clé personnelle + text_html: Vous n'avez pas accès à votre téléphone? %{link}. + personal_key_header_text: Entrez votre clé personnelle + personal_key_prompt: Vous pouvez utiliser cette clé personnelle une fois seulement. + Si vous avez toujours besoin d'un code après votre connexion, allez à la page + des réglages de votre compte pour en obtenir un nouveau. + please_confirm: Votre numéro de téléphone a été entré. Confirmez-le en entrant + le code de sécurité ci-dessous. + please_try_again_html: Veuillez essayer de nouveau dans %{time_remaining}. + totp_fallback: + sms_link_text: Obtenir un code via message texte + text_html: Si vous ne pouvez utiliser votre application d'authentification + maintenant, vous pouvez %{sms_link} ou %{voice_link}. + voice_link_text: obtenir un code par appel téléphonique + totp_header_text: Entrez votre code d'application d'authentification + totp_info: Utilisez n'importe quelle application d'authentification pour balayer + le code QR ci-dessous. + two_factor_setup: Ajoutez un numéro de téléphone + user: + new_otp_sent: Nous vous avons envoyé un code de sécurité à utilisation unique. diff --git a/config/locales/errors/en.yml b/config/locales/errors/en.yml index 23aa459e008..38a86bf6719 100644 --- a/config/locales/errors/en.yml +++ b/config/locales/errors/en.yml @@ -4,23 +4,23 @@ en: confirm_password_incorrect: Incorrect password. invalid_authenticity_token: Oops, something went wrong. Please sign in again. invalid_totp: Invalid code. Please try again. - max_password_attempts_reached: > - You've entered too many incorrect passwords. You can reset your password using the "Forgot your password?" link. + max_password_attempts_reached: You've entered too many incorrect passwords. You + can reset your password using the "Forgot your password?" link. messages: already_confirmed: was already confirmed, please try signing in blank: Please fill in this field. confirmation_code_incorrect: Incorrect code. Did you type it in correctly? - confirmation_invalid_token: > - Invalid confirmation link. Either the link expired or you - already confirmed your account. - confirmation_period_expired: > - Expired confirmation link. - You can click "Resend confirmation instructions" to get another one. + confirmation_invalid_token: Invalid confirmation link. Either the link expired + or you already confirmed your account. + confirmation_period_expired: Expired confirmation link. You can click "Resend + confirmation instructions" to get another one. expired: has expired, please request a new one format_mismatch: Please match the requested format. - improbable_phone: Invalid phone number. Please make sure you enter a 10-digit phone number. + improbable_phone: Invalid phone number. Please make sure you enter a valid phone + number. missing_field: Please fill in this field. - no_password_reset_profile: No profile has been recently deactivated due to a password reset + no_password_reset_profile: No profile has been recently deactivated due to a + password reset no_pending_profile: No profile is waiting for verification not_found: not found not_locked: was not locked diff --git a/config/locales/errors/es.yml b/config/locales/errors/es.yml index f3e3e2d66fc..bdac55909c4 100644 --- a/config/locales/errors/es.yml +++ b/config/locales/errors/es.yml @@ -1,39 +1,36 @@ --- es: errors: - confirm_password_incorrect: La contraseña proporcionada es incorrecta. - invalid_authenticity_token: Huy! Algo salió mal. Inicia sesión de nuevo. - invalid_totp: Se ha introducido un código no válido. Vuelve a intentarlo. - max_password_attempts_reached: > - Cerramos la sesión porque no proporcionó su contraseña correcta. Si - olvidó su contraseña, haga clic en "¿Olvidó su contraseña?" enlace abajo. + confirm_password_incorrect: La contraseña es incorrecta. + invalid_authenticity_token: "¡Oops! Algo salió mal. Inicie la sesión de nuevo." + invalid_totp: El código es inválido. Vuelva a intentarlo. + max_password_attempts_reached: Ha ingresado demasiadas contraseñas incorrectas. + Puede restablecer su contraseña usando el enlace "¿Olvidó su contraseña?". messages: - already_confirmed: Ya está confirmado, por favor intenta iniciar sesión. + already_confirmed: ya estaba confirmado, por favor intente iniciar una sesión blank: Por favor, rellenar este campo. - confirmation_code_incorrect: NOT TRANSLATED YET - confirmation_invalid_token: - El enlace de confirmación en el que ha hecho clic ya no es válido. Esto puede haber - sido causado al hacer clic en un enlace antiguo de su correo electrónico, o puede ser que ya haya - confirmado su cuenta. - confirmation_period_expired: - Has tardado más de %{period} para confirmar su dirección de correo electrónico. - Por favor haga clic en "Reenviar instrucciones de confirmación". - expired: Ha caducado, por favor solicite uno nuevo - format_mismatch: Por favor, coincidir con el formato solicitado. - improbable_phone: El número de teléfono no es válido. Asegúrese de ingresar un número de teléfono de 10 dígitos. - missing_field: Por favor, rellenar este campo. - no_password_reset_profile: Ningún perfil ha sido desactivado recientemente por un restablecimiento de contraseña. + confirmation_code_incorrect: El código es incorrecto. ¿Lo escribió correctamente? + confirmation_invalid_token: El enlace de confirmación no es válido. El enlace + expiró o usted ya ha confirmado su cuenta. + confirmation_period_expired: El enlace de confirmación expiró. Puede hacer clic + en "Reenviar instrucciones de confirmación" para obtener otro. + expired: ha caducado, por favor solicite uno nuevo + format_mismatch: Por favor, use el formato solicitado. + improbable_phone: NOT TRANSLATED YET + missing_field: Por favor, rellene este campo. + no_password_reset_profile: Ningún perfil ha sido desactivado recientemente por + un restablecimiento de contraseña. no_pending_profile: Ningún perfil está esperando verificación not_found: no encontrado not_locked: no estaba bloqueado not_saved: - one: '1 error prohibió este %{resource} de ser guardado:' - other: "%{count} errores prohibieron este %{resource} de ser guardado:" - otp_incorrect: El código secreto es incorrecto + one: '1 error no permitió guardar este %{resource}:' + other: "%{count} errores no permitieron guardar este %{resource}:" + otp_incorrect: El código es incorrecto. ¿Lo escribió correctamente? password_incorrect: La contraseña es incorrecta - personal_key_incorrect: El código de recuperación es incorrecto + personal_key_incorrect: La clave personal es incorrecta requires_phone: requiere que ingrese su número de teléfono. - unauthorized_authn_context: NOT TRANSLATED YET - unauthorized_service_provider: NOT TRANSLATED YET - weak_password: Su contraseña no es suficientemente fuerte. %{feedback} + unauthorized_authn_context: Contexto de autenticación no autorizado + unauthorized_service_provider: Proveedor de servicio no autorizado + weak_password: Su contraseña no es suficientemente segura. %{feedback} not_authorized: No está autorizado para realizar esta acción. diff --git a/config/locales/errors/fr.yml b/config/locales/errors/fr.yml new file mode 100644 index 00000000000..798dbd4f92c --- /dev/null +++ b/config/locales/errors/fr.yml @@ -0,0 +1,39 @@ +--- +fr: + errors: + confirm_password_incorrect: Mot de passe incorrect. + invalid_authenticity_token: Oups, une erreur s'est produite. Veuillez vous connecter + de nouveau. + invalid_totp: Code non valide. Veuillez essayer de nouveau. + max_password_attempts_reached: Vous avez inscrit des mots de passe incorrects + un trop grand nombre de fois. Vous pouvez réinitialiser votre mot de passe en + utilisant le lien « Vous avez oublié votre mot de passe? ». + messages: + already_confirmed: a déjà été confirmé, veuillez essayer de vous connecter + blank: Veuillez remplir ce champ. + confirmation_code_incorrect: Code non valide. L'avez-vous inscrit correctement? + confirmation_invalid_token: Lien de confirmation non valide. Le lien est expiré + ou vous avez déjà confirmé votre compte. + confirmation_period_expired: Lien de confirmation expiré. Vous pouvez cliquer + sur « Envoyer les instructions de confirmation de nouveau » pour en obtenir + un autre. + expired: est expiré, veuillez en demander un nouveau + format_mismatch: Veuillez vous assurer de respecter le format requis. + improbable_phone: NOT TRANSLATED YET + missing_field: Veuillez remplir ce champ. + no_password_reset_profile: Aucun profil récemment désactivé en raison d'une + réinitialisation de mot de passe + no_pending_profile: Aucun profil en attente de vérification + not_found: introuvable + not_locked: n'a pas été verrouillé + not_saved: + one: '1 erreur a interdit la sauvegarde de cette %{resource} :' + other: "%{count} des erreurs ont empêché la sauvegarde de cette %{resource} :" + otp_incorrect: Code non valide. L'avez-vous inscrit correctement? + password_incorrect: Mot de passe incorrect + personal_key_incorrect: Clé personnelle incorrecte + requires_phone: vous demande d'entrer votre numéro de téléphone. + unauthorized_authn_context: Contexte d'authentification non autorisé + unauthorized_service_provider: Fournisseur de service non autorisé + weak_password: Votre mot de passe n'est pas assez fort. %{feedback} + not_authorized: Vous n'êtes pas autorisé(e) à effectuer cette action. diff --git a/config/locales/event_types/en.yml b/config/locales/event_types/en.yml index f996c53eb75..cecdb7006da 100644 --- a/config/locales/event_types/en.yml +++ b/config/locales/event_types/en.yml @@ -6,8 +6,8 @@ en: authenticated_at: Signed in at %{service_provider} authenticator_disabled: Authenticator app disabled authenticator_enabled: Authenticator app enabled + eastern_timestamp: "%{timestamp} (Eastern)" email_changed: Email address changed phone_changed: Phone number changed phone_confirmed: Phone confirmed - eastern_timestamp: '%{timestamp} (Eastern)' usps_mail_sent: Letter sent diff --git a/config/locales/event_types/es.yml b/config/locales/event_types/es.yml index 39f23a3a646..a29a2c60409 100644 --- a/config/locales/event_types/es.yml +++ b/config/locales/event_types/es.yml @@ -2,12 +2,12 @@ es: event_types: account_created: Cuenta creada - account_verified: NOT TRANSLATED YET - authenticated_at: Se ha iniciado sesión en %{service_provider} - authenticator_disabled: La aplicación de autenticador está inhabilitada - authenticator_enabled: Aplicación de autenticador activada - email_changed: Dirección de correo electrónico cambiada + account_verified: Cuenta verificada + authenticated_at: Sesión iniciada en %{service_provider} + authenticator_disabled: La app de autenticación fue suspendida + authenticator_enabled: App de autenticación permitido + eastern_timestamp: "%{timestamp} (hora del Este)" + email_changed: Email cambiado phone_changed: Número de teléfono cambiado phone_confirmed: Teléfono confirmado - eastern_timestamp: '%{timestamp} (zona horaria del Este)' usps_mail_sent: Carta enviada diff --git a/config/locales/event_types/fr.yml b/config/locales/event_types/fr.yml new file mode 100644 index 00000000000..62131dacbc6 --- /dev/null +++ b/config/locales/event_types/fr.yml @@ -0,0 +1,13 @@ +--- +fr: + event_types: + account_created: Compte créé + account_verified: Compte vérifié + authenticated_at: Connecté à %{service_provider} + authenticator_disabled: Application d'authentification désactivée + authenticator_enabled: Application d'authentification activée + eastern_timestamp: "%{timestamp} (Eastern)" + email_changed: Adresse courriel modifiée + phone_changed: Numéro de téléphone modifié + phone_confirmed: Numéro de téléphone confirmé + usps_mail_sent: Lettre envoyée diff --git a/config/locales/forms/en.yml b/config/locales/forms/en.yml index 480bfc9b794..b736b739bdd 100644 --- a/config/locales/forms/en.yml +++ b/config/locales/forms/en.yml @@ -24,15 +24,16 @@ en: show: Show password personal_key: alternative: Don't have your personal key? - title: Enter your personal key - instructions: Please confirm you have a copy of your personal key by entering it below. confirmation_label: Personal key + instructions: Please confirm you have a copy of your personal key by entering + it below. + title: Enter your personal key registration: labels: email: Email address totp_setup: - totp_intro_html: > - When you sign in, you can get your security code from an authentication app. %{link} + totp_intro_html: When you sign in, you can get your security code from an authentication + app. %{link} totp_step_1: Open your authentication app totp_step_2: Enter this key in the app totp_step_3: Enter the code from the app @@ -41,7 +42,7 @@ en: personal_key: Personal key try_again: Use another phone number verify_profile: - name: Confirmation code instructions: Enter the ten-character code in the letter we sent you. + name: Confirmation code submit: Confirm account title: Confirm your account diff --git a/config/locales/forms/es.yml b/config/locales/forms/es.yml index 6facc87038c..be37a8653f4 100644 --- a/config/locales/forms/es.yml +++ b/config/locales/forms/es.yml @@ -4,13 +4,13 @@ es: buttons: back: Atrás continue: Continuar - disable: Inhabilitar + disable: Suspender edit: Editar - enable: Habilitar + enable: Permitir resend_confirmation: Reenviar instrucciones de confirmación - send_security_code: Enviar contraseña + send_security_code: Enviar código de seguridad submit: - confirm_change: Confirm change + confirm_change: Confirmar cambio default: Enviar update: Actualización confirmation: @@ -18,29 +18,31 @@ es: passwords: edit: buttons: - submit: Cambia la contraseña + submit: Cambiar la contraseña labels: password: Nueva contraseña - show: NOT TRANSLATED YET + show: Mostrar contraseña personal_key: - alternative: NOT TRANSLATED YET - title: NOT TRANSLATED YET - instructions: NOT TRANSLATED YET - confirmation_label: NOT TRANSLATED YET + alternative: "¿No tiene su clave personal?" + confirmation_label: Clave personal + instructions: Confirme que tiene una copia de su clave personal ingresándola + a continuación. + title: Ingrese su clave personal registration: labels: - email: Dirección de correo electrónico + email: Email totp_setup: - totp_intro_html: NOT TRANSLATED YET - totp_step_1: NOT TRANSLATED YET - totp_step_2: NOT TRANSLATED YET - totp_step_3: NOT TRANSLATED YET + totp_intro_html: Al iniciar una sesión, puede obtener el código de seguridad + de una app de autenticación. %{link} + totp_step_1: Abra su app de autenticación. + totp_step_2: Ingrese esta clave en la app. + totp_step_3: Ingrese el código de la app. two_factor: - code: Código de acceso único - personal_key: Código de recuperación - try_again: Inténtalo de nuevo + code: Código de seguridad de sólo un uso + personal_key: Clave personal + try_again: Use otro número de teléfono. verify_profile: - name: NOT TRANSLATED YET - title: NOT TRANSLATED YET - submit: NOT TRANSLATED YET - instructions: NOT TRANSLATED YET + instructions: Ingrese el código de 10 caracteres que le enviamos en la carta. + name: Código de confirmación + submit: Confirmar cuenta + title: Confirme su cuenta diff --git a/config/locales/forms/fr.yml b/config/locales/forms/fr.yml new file mode 100644 index 00000000000..fe721ae4650 --- /dev/null +++ b/config/locales/forms/fr.yml @@ -0,0 +1,49 @@ +--- +fr: + forms: + buttons: + back: Retour + continue: Continuer + disable: Désactiver + edit: Modifier + enable: Activer + resend_confirmation: Envoyer les instructions de confirmation de nouveau + send_security_code: Envoyer le code de sécurité + submit: + confirm_change: Confirmer le changement + default: Soumettre + update: Mettre à jour + confirmation: + show_hdr: Créez un mot de passe fort + passwords: + edit: + buttons: + submit: Changer le mot de passe + labels: + password: Nouveau mot de passe + show: Afficher le mot de passe + personal_key: + alternative: NOT TRANSLATED YET + confirmation_label: Clé personnelle + instructions: Veuillez confirmer que vous avez une copie de votre clé personnelle + en l'entrant ci-dessous. + title: Entrez votre clé personnelle + registration: + labels: + email: Adresse courriel + totp_setup: + totp_intro_html: Lorsque vous vous connectez, vous pouvez obtenir votre code + de sécurité d'une application d'authentification. %{link} + totp_step_1: Démarrez votre application d'authentification + totp_step_2: Entrez cette clé dans l'application + totp_step_3: Entrez le code à partir de l'application + two_factor: + code: Code de sécurité + personal_key: Clé personnelle + try_again: Utilisez un autre numéro de téléphone + verify_profile: + instructions: Entrez le code à dix caractères qui se trouve dans la lettre que + nous vous avons envoyée. + name: Code de confirmation + submit: Confirmer le compte + title: Confirmez votre compte diff --git a/config/locales/headings/en.yml b/config/locales/headings/en.yml index d1cf87dadb9..7dfa014dc14 100644 --- a/config/locales/headings/en.yml +++ b/config/locales/headings/en.yml @@ -1,6 +1,13 @@ --- en: headings: + account: + account_history: Account history + login_info: Your account + profile_info: Profile information + reactivate: Reactivate your account + two_factor: Two-factor authentication + verified_account: Verified Account confirmations: new: Send another confirmation email create_account_with_sp: @@ -14,13 +21,6 @@ en: change: Change your password confirm: Confirm your current password to continue forgot: Forgot your password? - account: - account_history: Account history - login_info: Your account - profile_info: Profile information - reactivate: Reactivate your account - two_factor: Two-factor authentication - verified_account: Verified Account personal_key: Here is your personal key registrations: enter_email: Start creating an account diff --git a/config/locales/headings/es.yml b/config/locales/headings/es.yml index dd431a09d86..76669445455 100644 --- a/config/locales/headings/es.yml +++ b/config/locales/headings/es.yml @@ -1,32 +1,32 @@ --- es: headings: + account: + account_history: Historial de cuenta + login_info: Su cuenta + profile_info: Información de perfil + reactivate: Reactive su cuenta + two_factor: Autenticación de dos factores + verified_account: Cuenta verificada confirmations: - new: Enviar otro correo electrónico de confirmación + new: Enviar otro email de confirmación create_account_with_sp: - sp_text: NOT TRANSLATED YET - create_account_without_sp: Crear una cuenta de login.gov + sp_text: está usando login.gov para ingresar de manera fácil y segura. + create_account_without_sp: Establezca una cuenta de login.gov edit_info: - email: Cambiar su correo electrónico - password: Cambiar su contraseña - phone: Ingresar su nuevo número de teléfono + email: Cambie su email + password: Cambie su contraseña + phone: Ingrese su nuevo número de teléfono passwords: - change: Cambiar su contraseña + change: Cambie su contraseña confirm: Confirme la contraseña actual para continuar - forgot: ¿Olvidó su contraseña? - account: - account_history: La historia de la cuenta - login_info: Información de la cuenta - profile_info: NOT TRANSLATED YET - reactivate: NOT TRANSLATED YET - two_factor: Autenticación de dos factores - verified_account: Cuenta verificada - personal_key: Asegúrese de que siempre puede iniciar sesión + forgot: "¿Olvidó su contraseña?" + personal_key: Aquí está su clave personal registrations: - enter_email: Empezar a crear una cuenta - session_timeout_warning: ¿Necesita mas tiempo? + enter_email: Empiece creando una cuenta + session_timeout_warning: "¿Necesita más tiempo?" sign_in_with_sp: Iniciar sesión para continuar con %{sp} sign_in_without_sp: Iniciar sesión totp_setup: - new: Configurar la autenticación de dos factores - verify_email: Revisar su correo electrónico + new: Permita una app de autenticación + verify_email: Revise su email diff --git a/config/locales/headings/fr.yml b/config/locales/headings/fr.yml new file mode 100644 index 00000000000..226fd4fedd5 --- /dev/null +++ b/config/locales/headings/fr.yml @@ -0,0 +1,32 @@ +--- +fr: + headings: + account: + account_history: Historique du compte + login_info: Votre compte + profile_info: Information du profil + reactivate: NOT TRANSLATED YET + two_factor: Authentification à deux facteurs + verified_account: Compte vérifié + confirmations: + new: Envoyer un autre courriel de confirmation + create_account_with_sp: + sp_text: utilise login.gov pour rendre la connexion plus facile et plus sécurisée. + create_account_without_sp: Créer un compte login.gov + edit_info: + email: Changez votre courriel + password: Changez votre mot de passe + phone: Entrez votre nouveau numéro de téléphone + passwords: + change: Changez votre mot de passe + confirm: Confirmez votre mot de passe actuel pour continuer + forgot: Vous avez oublié votre mot de passe? + personal_key: Voici votre clé personnelle + registrations: + enter_email: Commencer à créer le compte + session_timeout_warning: Vous avez besoin de plus de temps? + sign_in_with_sp: Connectez-vous pour continuer à %{sp} + sign_in_without_sp: Connexion + totp_setup: + new: Activer une application d'authentification + verify_email: Consultez vos courriels diff --git a/config/locales/help_text/en.yml b/config/locales/help_text/en.yml index 2cd0638c65f..784142c4ee3 100644 --- a/config/locales/help_text/en.yml +++ b/config/locales/help_text/en.yml @@ -1,14 +1,13 @@ --- en: help_text: - change_factor: > - Before you're able to edit your %{factor}, you will need to confirm your - password and receive a security code on your phone. + change_factor: Before you're able to edit your %{factor}, you will need to confirm + your password and receive a security code on your phone. requested_attributes: - intro_html: This is the only information %{app_name} will share with %{sp} address: Mailing address birthdate: Date of birth email: Email address full_name: Full name + intro_html: This is the only information %{app_name} will share with %{sp} phone: Phone number social_security_number: Social Security number diff --git a/config/locales/help_text/es.yml b/config/locales/help_text/es.yml index dc3ce1789fa..9812f0589ec 100644 --- a/config/locales/help_text/es.yml +++ b/config/locales/help_text/es.yml @@ -1,14 +1,13 @@ --- es: help_text: - change_factor: > - NOT TRANSLATED YET + change_factor: Antes de poder editar su% {factor}, deberá confirmar su contraseña + y recibir un código de seguridad en su teléfono. requested_attributes: - intro_html: NOT TRANSLATED YET - address: NOT TRANSLATED YET - birthdate: NOT TRANSLATED YET - email: NOT TRANSLATED YET - full_name: NOT TRANSLATED YET - phone: NOT TRANSLATED YET - social_security_number: NOT TRANSLATED YET - + address: Dirección de correo postal + birthdate: Fecha de nacimiento + email: Email + full_name: Nombre completo + intro_html: Esta es la única información que %{app_name} compartirá con %{sp} + phone: Teléfono + social_security_number: Número de Seguro Social diff --git a/config/locales/help_text/fr.yml b/config/locales/help_text/fr.yml new file mode 100644 index 00000000000..bf4426a6e57 --- /dev/null +++ b/config/locales/help_text/fr.yml @@ -0,0 +1,14 @@ +--- +fr: + help_text: + change_factor: Avant de pouvoir modifier votre %{factor}, vous devrez confirmer + votre mot de passe et recevoir un code de sécurité sur votre téléphone. + requested_attributes: + address: Adresse postale + birthdate: Date de naissance + email: Adresse courriel + full_name: Nom complet + intro_html: Il s'agit de la seule information que %{app_name} partagera avec + %{sp} + phone: Numéro de téléphone + social_security_number: Numéro de sécurité sociale diff --git a/config/locales/i18n/en.yml b/config/locales/i18n/en.yml new file mode 100644 index 00000000000..e1f2dc057ee --- /dev/null +++ b/config/locales/i18n/en.yml @@ -0,0 +1,8 @@ +--- +en: + i18n: + language: Language + locale: + en: English + es: Español + fr: Français diff --git a/config/locales/i18n/es.yml b/config/locales/i18n/es.yml new file mode 100644 index 00000000000..788a7662875 --- /dev/null +++ b/config/locales/i18n/es.yml @@ -0,0 +1,8 @@ +--- +es: + i18n: + language: Idioma + locale: + en: English + es: Español + fr: Français diff --git a/config/locales/i18n/fr.yml b/config/locales/i18n/fr.yml new file mode 100644 index 00000000000..3f538e17e75 --- /dev/null +++ b/config/locales/i18n/fr.yml @@ -0,0 +1,8 @@ +--- +fr: + i18n: + language: Langue + locale: + en: English + es: Español + fr: Français diff --git a/config/locales/idv/en.yml b/config/locales/idv/en.yml index fd3104718a8..99ad1b0168d 100644 --- a/config/locales/idv/en.yml +++ b/config/locales/idv/en.yml @@ -2,23 +2,24 @@ en: idv: buttons: - activate_by_phone: Activate by phone activate_by_mail: Activate by mail - continue: Continue identity verification + activate_by_phone: Activate by phone cancel: Cancel and return to your profile + continue: Continue identity verification help: Continue to Help Center mail: - send: Send a letter resend: Send another letter + send: Send a letter cancel: modal_header: Are you sure you want to cancel? warning_header: If you cancel now warning_points: - - We won’t be able to verify your identity - - We won't keep a record of your name, address, birth date, or Social Security number - - You won't be able to securely access your information using login.gov - - You’ll still have a login.gov account for your email address - - You can manage that account on your profile page + - We won’t be able to verify your identity + - We won't keep a record of your name, address, birth date, or Social Security + number + - You won't be able to securely access your information using login.gov + - You’ll still have a login.gov account for your email address + - You can manage that account on your profile page errors: bad_dob: Your date of birth must be a valid date. duplicate_ssn: An account already exists with the information you provided. @@ -26,12 +27,11 @@ en: hardfail: We were unable to verify your identity at this time. incorrect_password: The password you entered is not correct. invalid_ccn: Credit card number should be only last 8 digits. - missing_finance: You must provide a financial account number. mail_limit_reached: You have have requested too much mail in the last month. + missing_finance: You must provide a financial account number. pattern_mismatch: dob: Your date of birth must be entered in as mm/dd/yyyy - "personal-key": > - Please enter your personal key for this account. Example: ABC1-DEF2-G3HI-J456 + personal_key: 'Please enter your personal key for this account. Example: ABC1-DEF2-G3HI-J456' ssn: 'Your Social Security Number must be entered in as ###-##-####' zipcode: 'Your zipcode must be entered in as #####-####' form: @@ -62,9 +62,8 @@ en: zipcode: ZIP Code index: continue_link: Yes, continue - paragraph_1: > - We have partnered with a trusted third party company to verify this - information and your identity. + paragraph_1: We have partnered with a trusted third party company to verify + this information and your identity. prompt: Do you have all of this information available right now? section_1: bullet_1: Full name @@ -78,35 +77,30 @@ en: bullet_2: Auto loan account number bullet_3: Mortgage loan account number bullet_4: Home equity line of credit account number - footnote: > - You will never be charged any money and are not sharing any account balances or other - financial information with us. We only check the account number to help verify you. + footnote: You will never be charged any money and are not sharing any account + balances or other financial information with us. We only check the account + number to help verify you. header: Financial information subheader: 'We need one of the following financial account numbers:' section_3: bullet_1: Be in your name or the name of someone in your household - bullet_2_html: > - Not be a virtual phone, like Google Voice or Skype - bullet_3_html: > - Not be a pay-as-you-go phone + bullet_2_html: "Not be a virtual phone, like Google Voice + or Skype" + bullet_3_html: "Not be a pay-as-you-go phone" header: Phone line in your name - subheader: > - We use telephone company records to verify your identity. The phone - number you give us should: + subheader: 'We use telephone company records to verify your identity. The + phone number you give us should:' subheader: To verify your identity, we’ll ask you to answer a few questions messages: - activated_html: > - Your identity has been verified. If you need to change your verified - information, please %{link}. + activated_html: Your identity has been verified. If you need to change your + verified information, please %{link}. activated_link: contact us - cancel: > - To continue, %{app} needs to verify your identity. We need to collect - some basic personal information as well as some financial information from you - to complete this process. If you don’t have the information we need at this time - you can continue later. + cancel: To continue, %{app} needs to verify your identity. We need to collect + some basic personal information as well as some financial information from + you to complete this process. If you don’t have the information we need at + this time you can continue later. confirm: You have encrypted your verified data - dupe_ssn1: > - If you are getting this error, you may have already created and verified + dupe_ssn1: If you are getting this error, you may have already created and verified an account, but with a different email address. dupe_ssn2_html: Please %{link} with the email address you originally used. dupe_ssn2_link: sign out now and sign back in @@ -114,54 +108,51 @@ en: disclaimer: By continuing, you agree to let %{app_name} compare the account information you provide with third-party sources in order to verify your identity. - hint: "You can use Mastercard, Visa, Diner's Club, or JCB." - intro_ccn_html: > - Help us verify your identity by providing the last 8 digits of a - credit card. Learn more about %{intro_help_link}. + hint: You can use Mastercard, Visa, Diner's Club, or JCB. + intro_account: Help us verify your identity by providing an auto loan, mortgage, + or home equity loan number. intro_ccn_help: how your information will be used - intro_account: > - Help us verify your identity by providing an auto loan, mortgage, or home equity loan - number. + intro_ccn_html: Help us verify your identity by providing the last 8 digits + of a credit card. Learn more about %{intro_help_link}. no_account: Want to use a credit card instead? - no_account_info: Help us verify your identity by providing the last 8 digits of your - credit card. + no_account_info: Help us verify your identity by providing the last 8 digits + of your credit card. hardfail: We can’t verify your identity right now. hardfail4: You can also go to %{sp} for more help in accessing services. help_center: Visit our Help Center to learn more about verifying your account. + loading: Verifying your identity mail_sent: Your letter is on its way + personal_details_verified: Personal details verified! + personal_key: This is your new personal key. Write it down and keep it in a + safe place. You will need it if you ever lose your password. phone: alert: This phone line must be in_your_name: in your name or a family member's name - intro: We can only send a confirmation code to a telephone number linked to your - legal identity. + intro: We can only send a confirmation code to a telephone number linked to + your legal identity. phone_of_record: Phone of record prepaid: on a contract, not prepaid - same_as_2fa: > - This phone number can be the same one you used to set up your one-time - password as long as it meets the criteria above. - personal_key: > - This is your new personal key. Write it down and keep it in a safe - place. You will need it if you ever lose your password. + same_as_2fa: This phone number can be the same one you used to set up your + one-time password as long as it meets the criteria above. review: financial_info: Where is my financial account information? info_verified_html: We found records matching your %{phone_message} intro: Your verified information - select_verification_without_sp: - In order to protect your account from identity fraud, your profile - will not be activated until you enter a confirmation code. - select_verification_with_sp: To protect you from identity fraud, you can't use your - account at %{sp_name} until you activate it by entering a confirmation code. + select_verification_with_sp: To protect you from identity fraud, you can't use + your account at %{sp_name} until you activate it by entering a confirmation + code. + select_verification_without_sp: In order to protect your account from identity + fraud, your profile will not be activated until you enter a confirmation code. sessions: + no_pii: Do not use real personal information (demo purposes only) pii: personal information success: We found records matching your %{pii_message} - no_pii: Do not use real personal information (demo purposes only) usps: bad_address: I can't get mail at this address - byline: We will mail a letter with a confirmation code to your verified - address on file. + byline: We will mail a letter with a confirmation code to your verified address + on file. resend: Send me another letter. success: It should arrive in 5 to 10 business days. - personal_details_verified: Personal details verified! modal: attempts: one: You have 1 attempt remaining. @@ -170,24 +161,26 @@ en: fail: Okay warning: Try again financials: + fail: Your account is locked for 24 hours before you can + try to verify again. Check our Help section for more information about identity + verification. heading: We could not find records matching your financial information. - fail: > - Your account is locked for 24 hours before you can try to verify again. - Check our Help section for more information about identity verification. + timeout: Our request to verify your information timed out. warning: Please check the information you entered. phone: + fail: Your account is locked for 24 hours before you can + try to verify again. Check our Help section for more information about identity + verification. heading: We could not find records matching your phone information. - fail: > - Your account is locked for 24 hours before you can try to verify again. - Check our Help section for more information about identity verification. + timeout: Our request to verify your information timed out. warning: Please check the information you entered. sessions: + fail: Your account is locked for 24 hours before you can + try to verify again. heading: We could not find records matching your personal information. - fail: > - Your account is locked for 24 hours before you can try to verify again. - warning: > - Please check the information you entered. Common mistakes are an incorrect Social - Security Number, ZIP Code, or date of birth. + timeout: Our request to verify your information timed out. + warning: Please check the information you entered. Common mistakes are an + incorrect Social Security Number, ZIP Code, or date of birth. review: dob: Date of birth full_name: Full name @@ -204,12 +197,12 @@ en: hardfail: We were unable to verify your identity intro: Help us identify you mail: - verify: Want a letter? resend: Want another letter? + verify: Want a letter? phone: Phone number of record review: Review and submit select_verification: Activate your account - sessions: First, tell us about yourself session: phone: Get a code by telephone review: Encrypt your verified data by entering your password + sessions: First, tell us about yourself diff --git a/config/locales/idv/es.yml b/config/locales/idv/es.yml index efb28f903ff..e4c80d91f3d 100644 --- a/config/locales/idv/es.yml +++ b/config/locales/idv/es.yml @@ -2,170 +2,214 @@ es: idv: buttons: - activate_by_phone: NOT TRANSLATED YET - activate_by_mail: NOT TRANSLATED YET - continue: NOT TRANSLATED YET - cancel: NOT TRANSLATED YET - help: NOT TRANSLATED YET + activate_by_mail: Acitive por email + activate_by_phone: Active por teléfono + cancel: Cancele y regrese a su perfil + continue: Continúe la verificación de identidad + help: Continúe con el Centro de Ayuda mail: - send: NOT TRANSLATED YET - resend: NOT TRANSLATED YET + resend: Enviar otra carta + send: Enviar una carta cancel: - modal_header: NOT TRANSLATED YET - warning_header: NOT TRANSLATED YET - warning_points: - - NOT TRANSLATED YET + modal_header: "¿Está seguro que desea cancelar?" + warning_header: Si usted cancela ahora + warning_points: + - No podremos verificar su identidad + - No mantendremos un registro de su nombre, dirección, fecha de nacimiento o + número de Seguro Social + - No podrá acceder a su información con seguridad usando login.gov + - Usted todavía tiene una cuenta de login.gov con su email + - Puede administrar esa cuenta en su página de perfil errors: - bad_dob: NOT TRANSLATED YET - duplicate_ssn: NOT TRANSLATED YET - finance_number_length: NOT TRANSLATED YET - hardfail: NOT TRANSLATED YET - incorrect_password: NOT TRANSLATED YET - invalid_ccn: NOT TRANSLATED YET - missing_finance: NOT TRANSLATED YET - mail_limit_reached: NOT TRANSLATED YET + bad_dob: Su fecha de nacimiento debe ser una fecha válida. + duplicate_ssn: Ya existe una cuenta con la información que proporcionó. + finance_number_length: El número debe estar entre los dígitos %{mínimo} y %{máximo}. + hardfail: No pudimos verificar su identidad en este momento. + incorrect_password: La contraseña que ingresó no es correcta. + invalid_ccn: El número de la tarjeta de crédito debe ser de sólo los últimos + 8 dígitos. + mail_limit_reached: Usted ha solicitado demasiado correo en el último mes. + missing_finance: Debe proporcionar un número de cuenta financiera. pattern_mismatch: - dob: NOT TRANSLATED YET - "personal-key": NOT TRANSLATED YET - ssn: NOT TRANSLATED YET - zipcode: NOT TRANSLATED YET + dob: Su fecha de nacimiento debe ser ingresada en este formato mes/día/año. + personal_key: 'Introduzca su clave personal para esta cuenta. Ejemplo: ABC1-DEF2-G3HI-J456' + ssn: 'Su número de Seguro Social debe ser ingresado como ### - ## - ####' + zipcode: 'Su código postal debe ser ingresado como #####-####' form: - activate_by_mail: NOT TRANSLATED YET - address1: NOT TRANSLATED YET - address2: NOT TRANSLATED YET - auto_loan: NOT TRANSLATED YET - ccn: NOT TRANSLATED YET - city: NOT TRANSLATED YET - dob: NOT TRANSLATED YET - dob_hint: NOT TRANSLATED YET - first_name: NOT TRANSLATED YET - home_equity_line: NOT TRANSLATED YET - last_name: NOT TRANSLATED YET - mortgage: NOT TRANSLATED YET - no_alternate_phone_html: NOT TRANSLATED YET - password: NOT TRANSLATED YET - personal_details: NOT TRANSLATED YET - phone: NOT TRANSLATED YET - phone_label_aside: NOT TRANSLATED YET - previous_address_add: NOT TRANSLATED YET - previous_address_html: NOT TRANSLATED YET - select_financial_account: NOT TRANSLATED YET - ssn_label_html: NOT TRANSLATED YET - state: NOT TRANSLATED YET - use_ccn: NOT TRANSLATED YET - use_financial_account: NOT TRANSLATED YET - zipcode: NOT TRANSLATED YET + activate_by_mail: Activar mi cuenta por correo. + address1: Dirección de calle 1 + address2: Dirección de calle 2 (opcional) + auto_loan: Número de cuenta de préstamo de auto + ccn: Últimos 8 dígitos de una tarjeta de crédito + city: Ciudad + dob: Fecha de nacimiento + dob_hint: 'Ejemplo: 01/17/1964' + first_name: Nombre + home_equity_line: Número de la cuenta del préstamo sobre el valor neto de la + vivienda + last_name: Apellido + mortgage: Número de la cuenta de préstamo hipotecario + no_alternate_phone_html: No tengo otro número de teléfono. %{enlazar} + password: Contraseña + personal_details: Detalles personales + phone: Teléfono + phone_label_aside: Celular o teléfono fijo + previous_address_add: Añadir la dirección anterior + previous_address_html: "¿Se mudó en los últimos 3 meses?" + select_financial_account: Seleccione una cuenta financiera... + ssn_label_html: Número de Seguro Social %{tooltip} + state: Estado + use_ccn: Proporcione el número de tarjeta de crédito > + use_financial_account: "¿No tiene una tarjeta de crédito?" + zipcode: Código postal index: - continue_link: NOT TRANSLATED YET - paragraph_1: NOT TRANSLATED YET - prompt: NOT TRANSLATED YET + continue_link: Sí, continuar + paragraph_1: Nos hemos asociado con una empresa externa confiable para verificar + esta información y su identidad. + prompt: "¿Tiene toda esta información disponible ahora mismo?" section_1: - bullet_1: NOT TRANSLATED YET - bullet_2: NOT TRANSLATED YET - bullet_3: NOT TRANSLATED YET - bullet_4: NOT TRANSLATED YET - header: NOT TRANSLATED YET - subheader: NOT TRANSLATED YET + bullet_1: Nombre completo + bullet_2: Dirección postal + bullet_3: Fecha de nacimiento + bullet_4: Número de Seguro Social + header: Información personal + subheader: 'Comenzaremos con algunos datos personales como:' section_2: - bullet_1: NOT TRANSLATED YET - bullet_2: NOT TRANSLATED YET - bullet_3: NOT TRANSLATED YET - bullet_4: NOT TRANSLATED YET - footnote: NOT TRANSLATED YET - header: NOT TRANSLATED YET - subheader: NOT TRANSLATED YET + bullet_1: Últimos 8 dígitos de una tarjeta de crédito + bullet_2: Número de cuenta de préstamo de auto + bullet_3: Número de cuenta de préstamo hipotecario + bullet_4: Número de la cuenta del préstamo sobre el valor neto de la vivienda + footnote: Nunca se le cobrará ningún dinero y no compartirá ningún saldo de + cuenta u otra información financiera con nosotros. Solo verificamos el número + de cuenta para verificar su identidad. + header: Información financiera + subheader: 'Necesitamos uno de los siguientes números de cuenta financiera:' section_3: - bullet_1: NOT TRANSLATED YET - bullet_2_html: NOT TRANSLATED YET - bullet_3_html: NOT TRANSLATED YET - header: NOT TRANSLATED YET - subheader: NOT TRANSLATED YET - subheader: NOT TRANSLATED YET + bullet_1: Bajo su nombre o el nombre de alguien en su hogar + bullet_2_html: "No es un teléfono virtual, como Google Voice + o Skype" + bullet_3_html: "No es un teléfono de prepago" + header: Línea de teléfono a su nombre + subheader: 'Utilizamos registros de la compañía telefónica para verificar + su identidad. El número de teléfono que usted nos da debe:' + subheader: Para verificar su identidad, le pediremos que responda algunas preguntas messages: - activated_html: NOT TRANSLATED YET - activated_link: contact us - cancel: NOT TRANSLATED YET - confirm: NOT TRANSLATED YET - dupe_ssn1: NOT TRANSLATED YET - dupe_ssn2_html: NOT TRANSLATED YET - dupe_ssn2_link: NOT TRANSLATED YET + activated_html: Su identidad ha sido verificada. Si necesita cambiar la información + verificada, por favor, %{link}. + activated_link: Contáctenos + cancel: Para continuar %{app} necesita verificar su identidad. Necesitamos recopilar + información personal básica, así como su información financiera para completar + este proceso. Si no tiene la información que necesitamos en este momento, + puede continuar más tarde. + confirm: Usted ha encriptado sus datos verificados. + dupe_ssn1: Si recibe este error, es posible que ya haya creado y verificado + una cuenta, pero con un email diferente. + dupe_ssn2_html: Por favor %{link} con el email que utilizó originalmente. + dupe_ssn2_link: Cerrar ahora y volver a iniciar sesión finance: - disclaimer: NOT TRANSLATED YET - hint: NOT TRANSLATED YET - intro_ccn_html: NOT TRANSLATED YET - intro_ccn_help: NOT TRANSLATED YET - intro_account: NOT TRANSLATED YET - no_account: NOT TRANSLATED YET - no_account_info: NOT TRANSLATED YET - hardfail: NOT TRANSLATED YET - hardfail4: NOT TRANSLATED YET - help_center: NOT TRANSLATED YET - mail_sent: NOT TRANSLATED YET + disclaimer: Al continuar acepta que %{app_name} compare la información de + la cuenta que proporciona con fuentes de terceros para verificar su identidad. + hint: Puede utilizar Mastercard, Visa, Diner's Club o JCB. + intro_account: Ayúdenos a verificar su identidad proporcionando información + sobre un préstamo de auto, una hipoteca o un número de préstamo sobre el + valor neto de su casa. + intro_ccn_help: cómo se utilizará su información + intro_ccn_html: Ayúdenos a verificar su identidad proporcionando los últimos + 8 dígitos de una tarjeta de crédito. Obtenga más información sobre %{intro_help_link}. + no_account: "¿Desea usar una tarjeta de crédito?" + no_account_info: Ayúdenos a verificar su identidad proporcionando los últimos + 8 dígitos de su tarjeta de crédito. + hardfail: No podemos verificar su identidad en este momento. + hardfail4: También puede ir a %{sp} para obtener más ayuda y acceder a los servicios. + help_center: Visite nuestro Centro de Ayuda para obtener más información sobre + la verificación de su cuenta. + loading: NOT TRANSLATED YET + mail_sent: Su carta está en camino + personal_details_verified: "¡Detalles personales verificados!" + personal_key: Esta es su nueva clave personal. Escríbala y guárdela en un lugar + seguro. La necesitará si pierde su contraseña. phone: - alert: NOT TRANSLATED YET - in_your_name: NOT TRANSLATED YET - intro: NOT TRANSLATED YET - phone_of_record: NOT TRANSLATED YET - prepaid: NOT TRANSLATED YET - same_as_2fa: NOT TRANSLATED YET - personal_key: NOT TRANSLATED YET + alert: Esta línea telefónica debe + in_your_name: a su nombre o el nombre de un miembro de familia + intro: Sólo podemos enviar el código de confirmación al número de teléfono + vinculado a su identidad legal. + phone_of_record: Teléfono del registro + prepaid: en un contrato, no prepago + same_as_2fa: Este número de teléfono puede ser el mismo que utilizó para configurar + su contraseña de un uso único, siempre y cuando cumpla con los criterios + anteriores. review: - financial_info: NOT TRANSLATED YET - info_verified_html: NOT TRANSLATED YET - intro: NOT TRANSLATED YET - select_verification_without_sp: NOT TRANSLATED YET - select_verification_with_sp: NOT TRANSLATED YET + financial_info: "¿Dónde está la información de mi cuenta financiera?" + info_verified_html: Encontramos registros que coinciden con su %{teléfono_mensaje} + intro: Su información verificada + select_verification_with_sp: Para protegerlo de robo de identidad, no puede + utilizar su cuenta en %{sp_nombre} hasta que la active ingresando un código + de confirmación. + select_verification_without_sp: Para proteger su cuenta de robo de identidad, + su perfil no se activará hasta que ingrese un código de confirmación. sessions: - pii: NOT TRANSLATED YET - success: NOT TRANSLATED YET - no_pii: NOT TRANSLATED YET + no_pii: No utilice información personal real (sólo para propósitos de demostración) + pii: información personal + success: Encontramos registros que coinciden con su %{pii_mensaje} usps: - bad_address: NOT TRANSLATED YET - byline: NOT TRANSLATED YET - resend: NOT TRANSLATED YET - success: NOT TRANSLATED YET - personal_details_verified: NOT TRANSLATED YET + bad_address: No puedo recibir correo en esta dirección + byline: Le enviaremos una carta con un código de confirmación a su dirección + verificada en el archivo. + resend: Envíeme otra carta. + success: Debe llegar en 5 a 10 días laborales. modal: attempts: - one: NOT TRANSLATED YET - other: NOT TRANSLATED YET + one: Tiene usted 1 intento restante. + other: Tiene usted %{count} intentos restantes. button: - fail: NOT TRANSLATED YET - warning: NOT TRANSLATED YET + fail: Bueno + warning: Inténtelo de nuevo financials: - fail: NOT TRANSLATED YET - heading: NOT TRANSLATED YET - warning: NOT TRANSLATED YET + fail: Su cuenta está bloqueada por 24 horas antes de intentar + verificarla de nuevo. Consulte nuestra sección de Ayuda para obtener más + información sobre la verificación de identidad. + heading: No hemos podido encontrar registros que coincidan con su información + financiera. + timeout: NOT TRANSLATED YET + warning: Compruebe la información que ingresó. phone: - fail: NOT TRANSLATED YET - heading: NOT TRANSLATED YET - warning: NOT TRANSLATED YET + fail: Su cuenta está bloqueada por 24 horas antes de intentar + verificarla de nuevo. Consulte nuestra sección de Ayuda para obtener más + información sobre la verificación de identidad. + heading: No hemos podido encontrar registros que coincidan con la información + de su teléfono. + timeout: NOT TRANSLATED YET + warning: Compruebe la información que ingresó. sessions: - fail: NOT TRANSLATED YET - heading: NOT TRANSLATED YET - warning: NOT TRANSLATED YET + fail: Su cuenta está bloqueada por 24 horas antes de que + pueda intentar verificarla de nuevo. + heading: No hemos podido encontrar registros que coincidan con su información + personal. + timeout: NOT TRANSLATED YET + warning: Compruebe la información que ingresó. Los errores comunes son números + incorrectos de Seguro Social, código postal o fecha de nacimiento. review: - dob: NOT TRANSLATED YET - full_name: NOT TRANSLATED YET - mailing_address: NOT TRANSLATED YET - ssn: NOT TRANSLATED YET + dob: Fecha de nacimiento + full_name: Nombre completo + mailing_address: Dirección postal + ssn: Número de Seguro Social (SSN, sigla en inglés) titles: - activated: NOT TRANSLATED YET - cancel: NOT TRANSLATED YET - complete: NOT TRANSLATED YET - dupe: NOT TRANSLATED YET - expectations: NOT TRANSLATED YET - fail: NOT TRANSLATED YET - financials: NOT TRANSLATED YET - hardfail: NOT TRANSLATED YET - intro: NOT TRANSLATED YET + activated: Su identidad ha sido verificada. + cancel: No podemos verificar su identidad. + complete: Verificación de identidad completa + dupe: 'Error: Una cuenta puede que ya exista.' + expectations: A continuación, ayúdenos a identificarlo. + fail: No pudimos verificar su identidad. + financials: Proporcione un número de cuenta financiera. + hardfail: No pudimos verificar su identidad. + intro: Ayúdenos a identificarlo mail: - verify: NOT TRANSLATED YET - resend: NOT TRANSLATED YET - phone: NOT TRANSLATED YET - review: NOT TRANSLATED YET - select_verification: NOT TRANSLATED YET - sessions: NOT TRANSLATED YET + resend: "¿Desea otra carta?" + verify: "¿Desea una carta?" + phone: Número de teléfono del registro + review: Revise y envíe + select_verification: Active su cuenta session: - phone: NOT TRANSLATED YET - review: NOT TRANSLATED YET + phone: Obtener un código por teléfono + review: Encripte sus datos verificados ingresando su contraseña. + sessions: Primero, cuéntenos sobre usted diff --git a/config/locales/idv/fr.yml b/config/locales/idv/fr.yml new file mode 100644 index 00000000000..4391f11b268 --- /dev/null +++ b/config/locales/idv/fr.yml @@ -0,0 +1,227 @@ +--- +fr: + idv: + buttons: + activate_by_mail: Activer par la poste + activate_by_phone: Activer par téléphone + cancel: Annuler et retourner à votre profil + continue: Continuer la vérification d'identité + help: Continuer jusqu'au Centre d'aide + mail: + resend: Envoyer une autre lettre + send: Envoyer une lettre + cancel: + modal_header: Souhaitez-vous vraiment annuler? + warning_header: Si vous annulez maintenant + warning_points: + - Nous ne serons pas en mesure de vérifier votre identité + - Nous ne conserverons pas de dossier contenant votre nom, adresse, date de + naissance ou numéro de sécurité sociale + - Vous ne pourrez pas accéder à votre information de manière sécuritaire en + utilisant login.gov + - Vous conserverez un compte login.gov pour votre adresse courriel + - Vous pouvez gérer ce compte sur la page de votre profil + errors: + bad_dob: Votre date de naissance doit être une date valide. + duplicate_ssn: Un compte existe déjà avec l'information que vous avez fournie. + finance_number_length: Le nombre doit comprendre entre %{minimum} et %{maximum} + chiffres. + hardfail: Nous sommes dans l'incapacité de vérifier votre identité pour le moment. + incorrect_password: Le mot de passe que vous avez inscrit est incorrect. + invalid_ccn: Vous devez inscrire seulement les 8 derniers chiffres du numéro + de carte de crédit. + mail_limit_reached: Vous avez demandé trop de lettres au cours du dernier mois. + missing_finance: Vous devez fournir un numéro de compte bancaire. + pattern_mismatch: + dob: 'Votre date de naissance doit être inscrite de cette façon: mm/jj/aaaa' + personal_key: 'Veuillez inscrire votre clé personnelle pour ce compte, par + exemple : ABC1-DEF2-G3HI-J456' + ssn: 'Votre numéro de sécurité sociale doit être inscrit de cette façon : + ###-##-####' + zipcode: 'Votre code ZIP doit être inscrit de cette façon : #####-####' + form: + activate_by_mail: Activer mon compte par la poste. + address1: Adresse 1 + address2: Adresse 2 (optional) + auto_loan: Numéro du prêt automobile + ccn: Les 8 derniers chiffres d'une carte de crédit + city: Ville + dob: Date de naissance + dob_hint: 'exemple : 01/17/1964' + first_name: Prénom + home_equity_line: Numéro du compte de crédit hypothécaire + last_name: Nom de famille + mortgage: Numéro du compte de prêt hypothécaire + no_alternate_phone_html: Je n'ai pas de numéro de téléphone. %{link} + password: Mot de passe + personal_details: Information personnelle + phone: Numéro de téléphone + phone_label_aside: Ligne mobile ou fixe + previous_address_add: Inscrire votre adresse précédente + previous_address_html: Avez-vous déménagé au cours des strong3 derniers mois/strong? + select_financial_account: Sélectionnez un compte bancaire… + ssn_label_html: Numéro de sécurité sociale %{tooltip} + state: État + use_ccn: Fournissez un numéro de carte de crédit + use_financial_account: Vous n'avez pas de carte de crédit? + zipcode: Code ZIP + index: + continue_link: Oui, continuer + paragraph_1: Nous avons un partenariat avec une entreprise de confiance afin + de vérifier cette information ainsi que votre identité. + prompt: Est-ce que vous avez accès à toute cette information en ce moment? + section_1: + bullet_1: Nom complet + bullet_2: Adresse postale + bullet_3: Date de naissance + bullet_4: Numéro de sécurité sociale + header: Information personnelle + subheader: 'Nous allons commencer avec de l''information personnelle, comme:' + section_2: + bullet_1: Les 8 derniers chiffres d'une carte de crédit + bullet_2: Numéro du compte de prêt automobile + bullet_3: Numéro du compte de crédit hypothécaire + bullet_4: Numéro du compte de prêt hypothécaire + footnote: Vous n'aurez jamais à payer quoi que ce soit avec nous, ni à partager + le solde de vos comptes. Nous vérifions uniquement le numéro du compte pour + vérifier votre identité. + header: Information bancaire + subheader: 'Nous avons besoin de l''un des numéros de compte suivants:' + section_3: + bullet_1: Votre nom ou celui d'un membre de votre famille + bullet_2_html: strongNot/strong un téléphone virtuel, comme Google Voice ou + Skype + bullet_3_html: strongNot/strong un téléphone prépayé + header: Une ligne téléphonique à votre nom + subheader: 'Nous utilisons les informations des compagnies de téléphone pour + vérifier votre identité. Le numéro de téléphone que vous nous donnez devrait: ' + subheader: Pour vérifier votre identité, nous vous demandons de répondre à quelques + questions + messages: + activated_html: Votre identité a été vérifiée. Si vous souhaitez modifier votre + information vérifiée, veuillez %{link}. + activated_link: communiquer avec nous + cancel: Pour continuer, %{app} doit vérifier votre identité. Nous devons recueillir + quelques informations personnelles de base ainsi que certaines informations + financières pour compléter ce processus. Si vous n'avez pas cette information + sous la main, vous pouvez continuer plus tard. + confirm: Vous avez crypté vos données vérifiées + dupe_ssn1: Si vous recevez ce message d'erreur, il est possible que vous ayez + déjà créé et vérifié un compte, mais avec une adresse courriel différente. + dupe_ssn2_html: Veuillez %{link} avec l'adresse courriel que vous avez utilisée + originalement. + dupe_ssn2_link: déconnectez-vous puis connectez-vous à nouveau + finance: + disclaimer: En continuant, vous acceptez que %{app_name} compare l'information + du compte que vous avez fournie avec des sources tiers afin de vérifier + votre identité. + hint: Vous pouvez utiliser Mastercard, Visa, Diner's Club, ou JCB. + intro_account: Aidez-nous à vérifier votre identité en fournissant le numéro + de votre prêt automobile, de votre prêt hypothécaire, ou de votre crédit + hypothécaire. + intro_ccn_help: comment votre information sera utilisée + intro_ccn_html: Aidez-nous à vérifier votre identité en fournissant les 8 + derniers chiffres de votre carte de crédit. Suivez ce lien pour en apprendre + davantage %{intro_help_link}. + no_account: Vous préférez utiliser une carte de crédit? + no_account_info: Aidez-nous à vérifier votre identité en fournissant les 8 + derniers chiffres de votre carte de crédit. + hardfail: Nous ne pouvons pas vérifier votre identité pour le moment. + hardfail4: Vous pouvez également aller sur %{sp} où vous trouverez davantage + d'aide pour accéder à nos services. + help_center: Visitez notre Centre d'aide pour en apprendre davantage sur la + façon dont nous vérifions votre compte. + loading: NOT TRANSLATED YET + mail_sent: Votre lettre est en route + personal_details_verified: Information personnelle vérifiée! + personal_key: Il s'agit de votre nouvelle clé personnelle. Notez-la et conservez-la + dans un endroit sécuritaire. Vous en aurez besoin si vous perdez votre mot + de passe. + phone: + alert: Cette ligne téléphonique doit être + in_your_name: en votre nom ou celui d'un membre de votre famille + intro: Le seul numéro de téléphone auquel nous pouvons envoyer un code de + confirmation est celui qui est lié à votre identité légale. + phone_of_record: numéro de téléphone enregistré + prepaid: avec un contrat, et non prépayé + same_as_2fa: Ce numéro de téléphone peut être le même que celui que vous utilisez + pour configurer votre mot de passe à usage unique, tant et aussi longtemps + qu'il respecte les critères mentionnés plus haut. + review: + financial_info: Où se trouve l'information sur mon compte bancaire? + info_verified_html: Nous avons trouvé des données qui correspondent à votre + %{phone_message} + intro: Vos informations vérifiées + select_verification_with_sp: Afin de vous protéger des fraudes d'identité, vous + ne pouvez pas utiliser votre compte au %{sp_name} tant que vous ne l'aurez + pas activé en entrant votre code de confirmation. + select_verification_without_sp: Afin de protéger votre compte des fraudes liées + à l'identité, votre profil ne sera pas activé tant que vous n'aurez pas entré + votre code de confirmation. + sessions: + no_pii: N'utilisez pas de véritables données personnelles (il s'agit d'une + démonstration seulement) + pii: Information personnelle + success: Nous avons trouvé des données qui correspondent à %{pii_message} + usps: + bad_address: Je ne peux pas recevoir de courrier à cette adresse + byline: Nous posterons une lettre à l'adresse vérifiée dans nos dossiers. + Celle-ci contient un code de confirmation. + resend: Envoyez-moi une autre lettre. + success: Elle devrait arriver dans 5 à 10 jours ouvrables. + modal: + attempts: + one: Il ne vous reste qu' strongune tentative./strong + other: Il ne vous reste que strong%{count} tentatives./strong + button: + fail: OK + warning: Essayez à nouveau + financials: + fail: Votre compte est strongverrouillé pour 24 heures/strong au cours desquelles + vous ne pourrez pas y accéder. Consultez notre Centre d'aide pour plus d'information + sur la vérification de votre identité. + heading: Nous ne trouvons pas de données qui correspondent à vos données financières. + timeout: NOT TRANSLATED YET + warning: Veuillez vérifier l'information que vous avez fournie. + phone: + fail: Votre compte est strongverrouillé pour 24 heures/strong au cours desquelles + vous ne pourrez pas y accéder. Consultez notre Centre d'aide pour plus d'information + sur la vérification de votre identité. + heading: Nous ne trouvons pas de données qui correspondent à vos informations + téléphoniques. + timeout: NOT TRANSLATED YET + warning: Veuillez vérifier l'information que vous avez fournie. + sessions: + fail: Votre compte est strongverrouillé pour 24 heures/strong au cours desquelles + vous ne pourrez pas y accéder. + heading: Nous ne trouvons pas de données qui correspondent à vos informations + téléphoniques. + timeout: NOT TRANSLATED YET + warning: Veuillez vérifier l'information que vous avez fournie. Un numéro + de sécurité sociale, un code ZIP ou une date de naissance mal écrits sont + des erreurs communes. + review: + dob: Date de naissance + full_name: Nom complet + mailing_address: Adresse postale + ssn: Numéro de sécurité sociale (SSN) + titles: + activated: Votre identité a déjà été vérifiée + cancel: Nous ne pouvons pas vérifier votre identité + complete: Vérification de votre identité complétée + dupe: 'Erreur: un compte existe déjà' + expectations: Ensuite, aidez-vous à vous identifier + fail: Nous sommes dans l'incapacité de vérifier votre identité pour le moment + financials: Veuillez fournir un numéro de compte bancaire + hardfail: Nous sommes dans l'incapacité de vérifier votre identité + intro: Aidez-nous à vous identifier + mail: + resend: Vous voulez une autre lettre? + verify: Vous voulez une lettre? + phone: Numéro de téléphone enregistré + review: Réviser et soumettre + select_verification: Activer votre compte + session: + phone: Obtenez un code par téléphone + review: Encryptez vos données personnelles en entrant votre mot de passe + sessions: D'abord, dites-nous qui vous êtes diff --git a/config/locales/instructions/en.yml b/config/locales/instructions/en.yml index 6c75fcdb271..72780617456 100644 --- a/config/locales/instructions/en.yml +++ b/config/locales/instructions/en.yml @@ -1,50 +1,47 @@ --- en: instructions: - 2fa: + account: + reactivate: + begin: Let's get started. + explanation: 'When you created your account, we gave you a list of words and + asked you to store them in a safe place. It looked similar to this:' + intro: We take extra steps to keep your personal information secure and private, + so resetting your password takes a little extra effort. + modal: + copy: If you don't have your personal key, you will need to verify your + identity again. + heading: Don't have your personal key? + with_key: Do you have your personal key? + forgot_password: + close_window: You can close this browser window once you have reset your password. + go_back_to_mobile_app: To continue, please go back to the %{friendly_name} app + and sign in. + mfa: authenticator: accordion_header: Scan code with your mobile device - confirm_code_html: Enter the code from your authenticator app. If you have several accounts - set up in your app, enter the code corresponding to %{email} at - %{app}.%{tooltip} + confirm_code_html: Enter the code from your authenticator app. If you have + several accounts set up in your app, enter the code corresponding to %{email} + at %{app}.%{tooltip} or: or sms: - confirm_code_html: We sent it in a text message to %{number}. Need another code? - %{resend_code_link}. Message rates may apply. + confirm_code_html: We sent it in a text message to %{number}. Need another + code? %{resend_code_link}. Message rates may apply. fallback_html: If you can't get text messages right now, you can %{link} voice: confirm_code_html: We just called you at %{number}. Want us to call you again? %{resend_code_link} fallback_html: If you can't take a phone call right now, you can %{link} wrong_number_html: Entered the wrong phone number? %{link} - account: - reactivate: - begin: Let's get started. - explanation: > - When you created your account, we gave you a list of words and asked - you to store them in a safe place. It looked similar to this: - intro: > - We take extra steps to keep your personal information secure and private, - so resetting your password takes a little extra effort. - modal: - copy: If you don't have your personal key, you will need to verify your identity again. - heading: Don't have your personal key? - with_key: Do you have your personal key? - forgot_password: - close_window: You can close this browser window once you have reset your password. - go_back_to_mobile_app: To continue, please go back to the %{friendly_name} app and sign in. password: - forgot: > - Don’t know your password? Reset it after confirming your email address. + forgot: Don’t know your password? Reset it after confirming your email address. + help_text: The longer and more unusual the password, the harder it is to guess. + So avoid using common phrases. Also avoid repeating passwords from other online + accounts such as banks, email and social media. help_text_header: Password safety tips - help_text: > - The longer and more unusual the password, the harder it is to guess. So avoid using common - phrases. Also avoid repeating passwords from other online accounts such as banks, email and - social media. info: - lead: > - It must be at least %{min_length} characters long and not be a commonly used password. - That's it! + lead: It must be at least %{min_length} characters long and not be a commonly + used password. That's it! strength: i: Very weak ii: Weak @@ -52,9 +49,8 @@ en: intro: 'Password strength: ' iv: Good v: Great! - personal_key_html: > - This is the only way to regain access to your account if you lose your password or - phone. %{accent} personal_key_accent: Write it down or print it out. + personal_key_html: This is the only way to regain access to your account if you + lose your password or phone. %{accent} registration: email: Pick an address you want to use for government communications. diff --git a/config/locales/instructions/es.yml b/config/locales/instructions/es.yml index a54974c4138..831da38d27c 100644 --- a/config/locales/instructions/es.yml +++ b/config/locales/instructions/es.yml @@ -1,50 +1,60 @@ --- es: instructions: - 2fa: - authenticator: - accordion_header: NOT TRANSLATED YET - confirm_code_html: Ingrese el código de su aplicación de autenticador. Si tiene varias cuentas - configuradas en su aplicación, ingrese el código correspondiente a %{email} en - %{app}.%{tooltip} - or: NOT TRANSLATED YET - sms: - confirm_code_html: Lo enviamos a %{number}. ¿No recibió un código? - fallback_html: NOT TRANSLATED YET - voice: - confirm_code_html: NOT TRANSLATED YET - fallback_html: NOT TRANSLATED YET - wrong_number_html: ¿Ha ingresado el número de teléfono equivocado? %{link} account: reactivate: - begin: NOT TRANSLATED YET - explanation: NOT TRANSLATED YET - intro: NOT TRANSLATED YET + begin: Empecemos. + explanation: 'Cuando creó su cuenta le dimos una lista de palabras y le pedimos + que las guardara en un lugar seguro. Se parecía a esto:' + intro: Tomamos medidas adicionales para mantener su información personal segura + y privada, por lo que restablecer su contraseña requiere un pequeño esfuerzo + adicional. modal: - copy: NOT TRANSLATED YET - heading: NOT TRANSLATED YET - with_key: NOT TRANSLATED YET + copy: Si no tiene su clave personal, verifique su identidad nuevamente. + heading: Para continuar, por favor regrese a la %{friendly_name} app e inicie + una sesión. + with_key: "¿Tiene su clave personal?" forgot_password: - close_window: Puede cerrar esta ventana del navegador despues que haya restablecido su contraseña. - go_back_to_mobile_app: NOT TRANSLATED YET + close_window: Puede cerrar esta ventana del navegador después que haya restablecido + su contraseña. + go_back_to_mobile_app: Para continuar, por favor regrese a la %{friendly_name} + app e inicie una sesión. + mfa: + authenticator: + accordion_header: Escanear código con su dispositivo móvil + confirm_code_html: Ingrese el código de su app de autenticación. Si tiene + varias cuentas configuradas en su app, ingrese el código correspondiente + a %{email} en %{app}.%{tooltip} + or: o + sms: + confirm_code_html: Le enviamos en un mensaje de texto a %{number}. ¿Necesita + otro código? %{resend_code_link}. Puede estar sujeto a cargos de mensajería + y datos. + fallback_html: Si no puede recibir mensajes de texto ahora, puede %{link} + voice: + confirm_code_html: Acabamos de llamarle al %{number}. ¿Desea que le llamemos + de nuevo? %{resend_code_link} + fallback_html: Si no puede recibir una llamada de teléfono ahora, puede %{link} + wrong_number_html: "¿Ingresó el número de teléfono equivocado? %{link}" password: - forgot: > - Si no sabe su contraseña actual, puede restablecerla. Escriba la dirección de correo electrónico - asociada a su cuenta para recibir más instrucciones en la pantalla. - help_text_header: NOT TRANSLATED YET - help_text: NOT TRANSLATED YET + forgot: "¿No sabe su contraseña? Restablézcala después de confirmar su email." + help_text: Cuanto más larga y más inusual sea la contraseña, más difícil es + de adivinarla. Así que evite usar frases comunes. También evite repetir contraseñas + de otras cuentas en línea, por ejemplo de sus bancos, email y medios sociales. + help_text_header: Consejos de seguridad de contraseña info: - lead: Su contraseña debe tener al menos %{min_length} caracteres. + lead: Su contraseña debe tener al menos %{min_length} caracteres y no ser + una contraseña común. ¡Eso es todo! strength: i: Muy débil ii: Débil iii: Más o menos - intro: 'Seguridad de la contraseña: ' - iv: Bueno - v: Muy bueno! - personal_key_html: > - Escriba este código de recuperación y guárdelo en algún lugar seguro. Si no puede - acceder a su teléfono, puede usarlo en lugar de un código de acceso único para iniciar sesión. - personal_key_accent: NOT TRANSLATED YET + intro: 'Seguridad de la contraseña:' + iv: Buena + v: "¡Muy buena!" + personal_key_accent: Anótelo o imprímalo. + personal_key_html: Esta es la única manera de recuperar el acceso a su cuenta + si pierde su contraseña o teléfono. %{accent} registration: - email: Elija una dirección para usar en las comunicaciones del gobierno. + email: Elija una dirección que desee usar para manejar las comunicaciones del + Gobierno. diff --git a/config/locales/instructions/fr.yml b/config/locales/instructions/fr.yml new file mode 100644 index 00000000000..cad7c6b45d4 --- /dev/null +++ b/config/locales/instructions/fr.yml @@ -0,0 +1,59 @@ +--- +fr: + instructions: + account: + reactivate: + begin: NOT TRANSLATED YET + explanation: NOT TRANSLATED YET + intro: NOT TRANSLATED YET + modal: + copy: NOT TRANSLATED YET + heading: NOT TRANSLATED YET + with_key: NOT TRANSLATED YET + forgot_password: + close_window: Vous pourrez fermer cette fenêtre de navigateur lorsque vous aurez + réinitialisé votre mot de passe. + go_back_to_mobile_app: NOT TRANSLATED YET + mfa: + authenticator: + accordion_header: Balayez le code avec votre appareil mobile + confirm_code_html: Entrez le code à partir de votre application d'authentification. + Si vous avez plusieurs comptes configurés dans votre application, entrez + le code correspondant à %{email} à %{app}.%{tooltip} + or: or + sms: + confirm_code_html: Nous l'avons envoyé par message texte à %{number}. Vous + avez besoin d'un autre code? %{resend_code_link}. Les frais liés aux messages + texte peuvent s'appliquer. + fallback_html: Si vous ne pouvez recevoir de messages texte pour le moment, + vous pouvez %{link} + voice: + confirm_code_html: Nous venons de vous appeler au %{number}. Vous voulez que + nous vous appelions de nouveau? %{resend_code_link} + fallback_html: Si vous ne pouvez recevoir d'appels pour le moment, vous pouvez + %{link} + wrong_number_html: Vous avez entré un mauvais numéro de téléphone? %{link} + password: + forgot: Vous ne connaissez pas votre mot de passe? Réinitialisez-le après avoir + confirmé votre adresse courriel. + help_text: Plus long et inhabituel est le mot de passe, plus difficile il sera + à trouver. Évitez donc d'utiliser des phrases communes. Évitez aussi de répéter + des mots de passe d'autres comptes en ligne comme les comptes bancaires, les + comptes courriel et les comptes de médias sociaux. + help_text_header: Conseils sur la sécurité du mot de passe + info: + lead: Il doit avoir une longueur minimale de %{min_length} caractères et ne + pas être un mot de passe couramment utilisé. C'est tout! + strength: + i: Très faible + ii: Faible + iii: Correct + intro: 'Force du mot de passe : ' + iv: Bonne + v: Excellente! + personal_key_accent: Notez-la ou imprimez-la. + personal_key_html: Il s'agit de la seule façon de récupérer l'accès à votre compte + si vous perdez votre mot de passe ou votre téléphone. %{accent} + registration: + email: Choisissez une adresse que vous souhaitez utiliser pour les communications + avec le gouvernement. diff --git a/config/locales/jobs/en.yml b/config/locales/jobs/en.yml index 5405f8f767d..fc312f77076 100644 --- a/config/locales/jobs/en.yml +++ b/config/locales/jobs/en.yml @@ -1,10 +1,10 @@ --- en: jobs: + sms_otp_sender_job: + message: "%{code} is your %{app} one-time security code." voice_otp_sender_job: - message_final: > - Hello! Your login.gov one time security code is, %{code}, again, your - security code is, %{code}, goodbye! - message_repeat: > - Hello! Your login.gov one time security code is, %{code}, again, your - security code is, %{code}. Press 1 to repeat your code. + message_final: Hello! Your login.gov one time security code is, %{code}, again, + your security code is, %{code}, goodbye! + message_repeat: Hello! Your login.gov one time security code is, %{code}, again, + your security code is, %{code}. Press 1 to repeat your code. diff --git a/config/locales/jobs/es.yml b/config/locales/jobs/es.yml index 6904a69ef59..2f321118986 100644 --- a/config/locales/jobs/es.yml +++ b/config/locales/jobs/es.yml @@ -1,6 +1,11 @@ --- es: jobs: + sms_otp_sender_job: + message: "%{code} es su %{app} código de seguridad de sólo un uso." voice_otp_sender_job: - message_final: NOT TRANSLATED YET - message_repeat: NOT TRANSLATED YET + message_final: "¡Hola! Su código de seguridad de login.gov para uso único es, + %{code}, nuevamente, su código de seguridad es, % {code}, ¡adiós!" + message_repeat: "¡Hola! Su código de seguridad de login.gov para uso único es + %{code}, nuevamente, su código de seguridad es % {code}. Presione 1 para repetir + su código." diff --git a/config/locales/jobs/fr.yml b/config/locales/jobs/fr.yml new file mode 100644 index 00000000000..fe6a39446d9 --- /dev/null +++ b/config/locales/jobs/fr.yml @@ -0,0 +1,11 @@ +--- +fr: + jobs: + sms_otp_sender_job: + message: "%{code} est votre code de sécurité à utilisation unique pour %{app}" + voice_otp_sender_job: + message_final: Bonjour! Votre code de sécurité à utilisation unique de login.gov + est, %{code}, de nouveau, votre code de sécurité est, %{code}, au revoir! + message_repeat: Bonjour! Votre code de sécurité à utilisation unique de login.gov + est, %{code}, de nouveau, votre code de sécurité est, %{code}. Appuyez sur 1 + pour répéter votre code. diff --git a/config/locales/links/en.yml b/config/locales/links/en.yml index e70bd4206fe..16845afc752 100644 --- a/config/locales/links/en.yml +++ b/config/locales/links/en.yml @@ -6,6 +6,9 @@ en: with_key: I have my key without_key: I don't have my key back_to_sp: Back to %{sp} + cancel: Cancel + cancel_account_creation: "‹ Cancel account creation" + cancel_idv: "‹ Cancel account verification" contact: Contact copy: Copy create_account: Create account @@ -14,23 +17,21 @@ en: passwords: forgot: Forgot your password? phone_confirmation: - auth_app_fallback_html: ' or %{link}.' + auth_app_fallback_html: " or %{link}." fallback_to_sms_html: Send me a text message with the code instead - fallback_to_voice_html: If you can't get text message right now, you can get a security code via %{link} + fallback_to_voice_html: If you can't get text message right now, you can get + a security code via %{link} privacy_policy: Privacy & security remove: Remove resend: Resend email reverify: Please verify your identity again. sign_in: Sign in sign_out: Sign out - cancel: Cancel - cancel_account_creation: ‹ Cancel account creation - cancel_idv: ‹ Cancel account verification two_factor_authentication: app: get a security code via authentication app - voice: get a security code via phone call - sms: get a security code via text message resend_code: sms: Get another text message voice: Get another phone call + sms: get a security code via text message + voice: get a security code via phone call what_is_totp: What is an authentication app? diff --git a/config/locales/links/es.yml b/config/locales/links/es.yml index da1070539d6..1fcc5b5bf8f 100644 --- a/config/locales/links/es.yml +++ b/config/locales/links/es.yml @@ -3,34 +3,35 @@ es: links: account: reactivate: - with_key: NOT TRANSLATED YET - without_key: NOT TRANSLATED YET - back_to_sp: "Volver a %{sp}" + with_key: Tengo mi clave + without_key: No tengo mi clave + back_to_sp: Volver a %{sp} + cancel: Cancelar + cancel_account_creation: "< Cancelar la creación de cuenta" + cancel_idv: "< Cancelar la verificación de cuenta" contact: Contactar - copy: NOT TRANSLATED YET + copy: Copiar create_account: Crear cuenta help: Ayuda next: Siguiente passwords: - forgot: ¿Olvidó su contraseña? + forgot: "¿Olvidó su contraseña?" phone_confirmation: - auth_app_fallback_html: NOT TRANSLATED YET - fallback_to_sms_html: Envíame un mensaje de texto con el código en su lugar - fallback_to_voice_html: Llámame con el código en su lugar + auth_app_fallback_html: o %{link}. + fallback_to_sms_html: Envíeme un mensaje de texto con el código en su lugar + fallback_to_voice_html: Si no puede recibir un mensaje de texto ahora mismo, + puede obtener un código de seguridad a través de %{link} privacy_policy: Privacidad y seguridad - remove: NOT TRANSLATED YET + remove: Retirar resend: Enviar de nuevo - reverify: NOT TRANSLATED YET + reverify: Verifique su identidad nuevamente. sign_in: Iniciar sesión sign_out: Cerrar sesión - cancel: Cancelar - cancel_account_creation: NOT TRANSLATED YET - cancel_idv: NOT TRANSLATED YET two_factor_authentication: - app: NOT TRANSLATED YET - voice: NOT TRANSLATED YET - sms: NOT TRANSLATED YET + app: Obtener un código de seguridad mediante la app de autenticación resend_code: - sms: NOT TRANSLATED YET - voice: NOT TRANSLATED YET - what_is_totp: NOT TRANSLATED YET + sms: Obtener otro mensaje de texto + voice: Obtener otra llamada telefónica + sms: Obtener un código de seguridad a través de un mensaje de texto + voice: Obtener un código de seguridad a través de una llamada telefónica + what_is_totp: "¿Qué es una app de autenticación?" diff --git a/config/locales/links/fr.yml b/config/locales/links/fr.yml new file mode 100644 index 00000000000..16dc81e3b81 --- /dev/null +++ b/config/locales/links/fr.yml @@ -0,0 +1,37 @@ +--- +fr: + links: + account: + reactivate: + with_key: NOT TRANSLATED YET + without_key: NOT TRANSLATED YET + back_to_sp: Retour à %{sp} + cancel: Annuler + cancel_account_creation: "‹ Annuler la création du compte" + cancel_idv: "‹ Annuler la vérification du compte" + contact: Contact + copy: Copier + create_account: Créer un compte + help: Aide + next: Suivant + passwords: + forgot: Vous avez oublié votre mot de passe? + phone_confirmation: + auth_app_fallback_html: " ou %{link}." + fallback_to_sms_html: Envoyez-moi plutôt un SMS contenant le code + fallback_to_voice_html: Si vous ne pouvez recevoir de message texte pour le + moment, vous pouvez obtenir un code de sécurité par %{link} + privacy_policy: Confidentialité et sécurité + remove: Retirer + resend: Envoyer le courriel de nouveau + reverify: NOT TRANSLATED YET + sign_in: Connexion + sign_out: Déconnexion + two_factor_authentication: + app: obtenir un code de sécurité par l'application d'authentification + resend_code: + sms: Recevoir un autre SMS + voice: Recevoir un autre appel téléphonique + sms: obtenir un code de sécurité par SMS + voice: obtenir un code par un appel téléphonique + what_is_totp: Qu'est-ce qu'une application d'authentification? diff --git a/config/locales/mailer/en.yml b/config/locales/mailer/en.yml index 2091baa76ca..7bcb630ff1e 100644 --- a/config/locales/mailer/en.yml +++ b/config/locales/mailer/en.yml @@ -5,25 +5,23 @@ en: confirmation_instructions: first_sentence: confirmed: Trying to change your email address? - reset_requested: > - Your %{app} account has been reset by a tech support representative. + reset_requested: Your %{app} account has been reset by a tech support representative. To continue, you must confirm your email address. - unconfirmed: Thanks for creating an account. + unconfirmed: Thanks for creating an account. footer: This link will expire in %{confirmation_period}. - header: > - %{intro} Please click the link below or copy and paste the entire link into your browser. + header: "%{intro} Please click the link below or copy and paste the entire link + into your browser." link_text: Confirm your email address email_change_notice: subject: Change your email address email_reuse_notice: - subject: This email address is already associated with an account. + subject: This email address is already associated with an account. help: For more help, visit %{link}. no_reply: Please do not reply to this message. privacy_policy: Privacy policy reset_password: footer: This link expires in %{expires} hours. - header: > - To finish resetting your password, please click the link below - or copy and paste the entire link into your browser. + header: To finish resetting your password, please click the link below or copy + and paste the entire link into your browser. link_text: Reset your password sent_from: Sent from %{app} diff --git a/config/locales/mailer/es.yml b/config/locales/mailer/es.yml index 77988d2184b..2a946221188 100644 --- a/config/locales/mailer/es.yml +++ b/config/locales/mailer/es.yml @@ -1,37 +1,27 @@ --- es: mailer: - about: Sobre %{app} + about: Acerca de %{app} confirmation_instructions: first_sentence: - confirmed: > - Para finalizar la actualización de tu cuenta de %{app}, debe confirmar dirección de - correo electrónico dentro de %{confirmation_period} de recibir este mensaje. - reset_requested: > - Su cuenta de %{app} ha sido restablecida por un representante de soporte técnico. - Para continuar, debe confirmar su dirección de correo electrónico. - unconfirmed: > - Para continuar creando su cuenta de %{app}, debe confirmar su dirección de - correo electrónico dentro de %{confirmation_period} de recibir este mensaje. - footer: Tome en cuenta que este enlace expira en %{período de confirmación}. - header: > - %{intro} Para confirmar su dirección de correo electrónico, haga clic en el enlace abajo - o puede copiar y pegar el enlace completo en su navegador. - link_text: Confirmar esta dirección de correo electrónico + confirmed: "¿Desea cambiar su email?" + reset_requested: Su cuenta de %{app} ha sido restablecida por un representante + de soporte técnico. Para continuar, debe confirmar su email. + unconfirmed: Gracias por crear una cuenta. + footer: Este enlace expira en %{confirmation_period}. + header: "%{intro} Haga clic en el enlace de abajo o copie y pegue el enlace + completo en su navegador." + link_text: Confirme su email email_change_notice: - subject: Cambiar su direccion de correo electronico + subject: Cambie su email email_reuse_notice: - subject: Confirmar su dirección de correo electrónico - help: > - Para obtener más ayuda, póngase en contacto con el centro de contacto de clientes de %{app} a través - del formulario web en %{link}. + subject: Este email ya está asociado a una cuenta. + help: Para obtener más ayuda, visite %{link}. no_reply: Por favor, no responda a este mensaje. privacy_policy: Política de privacidad reset_password: - footer: > - Tome en cuenta que este enlace caduca en %{expires} horas. - header: > - Ha solicitado que restablezca la contraseña de su cuenta. Para confirmar - su solicitud, haga clic en el enlace abajo o puede copiar y pegar el enlace completo en su navegador. - link_text: Restablecer su contraseña + footer: Este enlace expira en %{expires} horas. + header: Para terminar de restablecer su contraseña, haga clic en el enlace de + abajo o copie y pegue el enlace completo en su navegador. + link_text: Restablezca su contraseña sent_from: Enviado desde %{app} diff --git a/config/locales/mailer/fr.yml b/config/locales/mailer/fr.yml new file mode 100644 index 00000000000..6dbfbf8d340 --- /dev/null +++ b/config/locales/mailer/fr.yml @@ -0,0 +1,28 @@ +--- +fr: + mailer: + about: À propos de %{app} + confirmation_instructions: + first_sentence: + confirmed: Vous tentez de changer votre adresse courriel? + reset_requested: Votre compte %{app} a été réinitialisé par un représentant + du soutien technique. Pour continuer, vous devez confirmer votre adresse + courriel. + unconfirmed: Merci d'avoir créé un compte. + footer: Ce lien expirera dans %{confirmation_period}. + header: "%{intro} Veuillez cliquer sur le lien ci-dessous ou copier et coller + le lien complet dans votre navigateur." + link_text: Confirmez votre adresse courriel + email_change_notice: + subject: Changez votre adresse courriel + email_reuse_notice: + subject: Cette adresse courriel est déjà associée à un compte. + help: Pour plus d'aide, visitez %{link}. + no_reply: Veuillez ne pas répondre à ce message. + privacy_policy: Politique de confidentialité + reset_password: + footer: Ce lien expire dans %{expires} heures. + header: Pour terminer la réinitialisation de votre mot de passe, veuillez cliquer + sur le lien ci-dessous ou copier et coller le lien complet dans votre navigateur. + link_text: Réinitialisez votre mot de passe + sent_from: Envoyé à partir de %{app} diff --git a/config/locales/notices/en.yml b/config/locales/notices/en.yml index 98188b14bbd..98ebefedb49 100644 --- a/config/locales/notices/en.yml +++ b/config/locales/notices/en.yml @@ -1,53 +1,47 @@ --- en: notices: - account_recovery: Great! You have your personal key. - dap_html: > - - + account_reactivation: Great! You have your personal key. + dap_html: " " forgot_password: - use_diff_email: - link: create a new account - text_html: Or, %{link} using a different email address. - first_paragraph_end: > - with a link to reset your password. Follow the link to continue - resetting your password. + first_paragraph_end: with a link to reset your password. Follow the link to + continue resetting your password. first_paragraph_start: We sent an email to no_email_sent_explanation_start: Didn’t receive an email? resend_email_success: We sent another password reset email. + use_diff_email: + link: create a new account + text_html: Or, %{link} using a different email address. password_changed: You changed your password. resend_confirmation_email: success: We sent another confirmation email. send_code: personal_key: You have a new personal key. - timeout_warning: - signed_in: - continue: Keep me signed in - sign_out: Sign me out - message_html: > - For your security, we will sign you out in %{time_left_in_session} unless - you tell us otherwise. - partially_signed_in: - continue: Continue sign in - sign_out: Cancel sign in - message_html: > - For your security, in %{time_left_in_session} we will - cancel your sign in. - session_cleared: > - For your security, we clear what you entered if you don't move to a new page within %{minutes} minutes. + session_cleared: For your security, we clear what you entered if you don't move + to a new page within %{minutes} minutes. signed_up_but_unconfirmed: - first_paragraph_end: > - with a link to confirm your email address. Follow the link to continue - creating your account. + first_paragraph_end: with a link to confirm your email address. Follow the link + to continue creating your account. first_paragraph_start: We sent an email to no_email_sent_explanation_start: Didn’t receive it? terms_of_service: link: Security Practices and Privacy Act Statement + timeout_warning: + partially_signed_in: + continue: Continue sign in + message_html: For your security, in %{time_left_in_session} we will cancel + your sign in. + sign_out: Cancel sign in + signed_in: + continue: Keep me signed in + message_html: For your security, we will sign you out in %{time_left_in_session} + unless you tell us otherwise. + sign_out: Sign me out totp_configured: You enabled an authentication app. totp_disabled: You disabled your authentication app. use_diff_email: link: use a different email address text_html: Or, %{link} - session_timedout: > - We signed you out. For your security, %{app} ends your session + session_timedout: We signed you out. For your security, %{app} ends your session when you haven’t moved to a new page for %{minutes} minutes. diff --git a/config/locales/notices/es.yml b/config/locales/notices/es.yml index 46c9667b8f6..25242e0981f 100644 --- a/config/locales/notices/es.yml +++ b/config/locales/notices/es.yml @@ -1,41 +1,47 @@ --- es: notices: - account_recovery: NOT TRANSLATED YET - dap_html: NOT TRANSLATED YET + account_reactivation: "¡Estupendo! Tiene su clave personal." + dap_html: " " forgot_password: + first_paragraph_end: con un enlace para restablecer su contraseña. Siga el enlace + para continuar restableciendo su contraseña. + first_paragraph_start: Enviamos un email a + no_email_sent_explanation_start: "¿No recibió un email?" + resend_email_success: Enviamos otro email para restablecer la contraseña. use_diff_email: - link: NOT TRANSLATED YET - text_html: NOT TRANSLATED YET - first_paragraph_end: > - NOT TRANSLATED YET - first_paragraph_start: NOT TRANSLATED YET - no_email_sent_explanation_start: NOT TRANSLATED YET - resend_email_success: NOT TRANSLATED YET - password_changed: NOT TRANSLATED YET + link: crear una cuenta nueva + text_html: O, %{link} utilizando un email diferente. + password_changed: Ha cambiado su contraseña. resend_confirmation_email: - success: NOT TRANSLATED YET + success: Enviamos otro email de confirmación. send_code: - personal_key: NOT TRANSLATED YET - timeout_warning: - signed_in: - continue: NOT TRANSLATED YET - sign_out: NOT TRANSLATED YET - message_html: NOT TRANSLATED YET - partially_signed_in: - continue: NOT TRANSLATED YET - sign_out: NOT TRANSLATED YET - message_html: NOT TRANSLATED YET - session_cleared: NOT TRANSLATED YET + personal_key: Tiene una nueva clave personal. + session_cleared: Para su seguridad, borramos lo que ingresó si no pasa a una página + nueva dentro de %{minutes} minutos. signed_up_but_unconfirmed: - first_paragraph_end: NOT TRANSLATED YET - first_paragraph_start: NOT TRANSLATED YET - no_email_sent_explanation_start: NOT TRANSLATED YET + first_paragraph_end: con un enlace para confirmar su email. Siga el enlace para + continuar creando su cuenta. + first_paragraph_start: Enviamos un email a + no_email_sent_explanation_start: "¿No lo recibió?" terms_of_service: - link: NOT TRANSLATED YET - totp_configured: NOT TRANSLATED YET - totp_disabled: NOT TRANSLATED YET + link: Prácticas de Seguridad y Declaración de Privacidad + timeout_warning: + partially_signed_in: + continue: Continuar el inicio de sesión + message_html: Para su seguridad, en %{time_left_in_session} cancelaremos su + acceso. + sign_out: Cancelar el inicio de sesión + signed_in: + continue: Manténgame conectado + message_html: Para su seguridad, terminaremos su sesión en% {time_left_in_session} + a menos que nos indique lo contrario. + sign_out: Desconécteme + totp_configured: Ha permitido una app de autenticación. + totp_disabled: Ha suspendido su app de autenticación. use_diff_email: - link: NOT TRANSLATED YET - text_html: NOT TRANSLATED YET - session_timedout: NOT TRANSLATED YET + link: use un email diferente + text_html: O %{link} + session_timedout: Hemos terminado su sesión. Para su seguridad, %{app} cierra su + sesión cuando usted no pasa a una nueva página durante %{minutes} minutos. diff --git a/config/locales/notices/fr.yml b/config/locales/notices/fr.yml new file mode 100644 index 00000000000..3db841b8198 --- /dev/null +++ b/config/locales/notices/fr.yml @@ -0,0 +1,50 @@ +--- +fr: + notices: + account_reactivation: NOT TRANSLATED YET + dap_html: " " + forgot_password: + first_paragraph_end: avec un lien pour réinitialiser votre mot de passe. Suivez + le lien pour continuer à réinitialiser votre mot de passe. + first_paragraph_start: Nous avons envoyé un courriel à + no_email_sent_explanation_start: Vous n'avez pas reçu de courriel? + resend_email_success: Nous avons envoyé un autre courriel de réinitialisation + de mot de passe. + use_diff_email: + link: Créer un nouveau compte + text_html: Ou, %{link} en utilisant une adresse courriel différente. + password_changed: Vous avez changé votre mot de passe. + resend_confirmation_email: + success: Nous avons envoyé un autre courriel de confirmation. + send_code: + personal_key: Vous avez une nouvelle clé personnelle. + session_cleared: Pour votre sécurité, nous effacerons l'information que vous avez + entrée si vous ne vous déplacez pas vers une nouvelle page dans les %{minutes} + prochaines minutes. + signed_up_but_unconfirmed: + first_paragraph_end: avec un lien pour confirmer votre adresse courriel. Suivez + le lien pour continuer à créer votre compte. + first_paragraph_start: Nous avons envoyé un courriel à + no_email_sent_explanation_start: Vous n'avez pas reçu de courriel? + terms_of_service: + link: Pratiques en matière de sécurité et énoncé concernant la Loi sur la protection + des renseignements personnels + timeout_warning: + partially_signed_in: + continue: Continuer la connexion + message_html: Pour votre sécurité, nous annulerons votre connexion dans %{time_left_in_session}. + sign_out: Annuler la connexion + signed_in: + continue: Gardez ma connexion active + message_html: Pour votre sécurité, nous vous déconnecterons dans %{time_left_in_session}, + sauf en cas d'avis contraire de votre part. + sign_out: Déconnectez-moi + totp_configured: Vous avez activé l'application d'authentification. + totp_disabled: Vous avez désactivé l'application d'authentification. + use_diff_email: + link: utilisez une adresse courriel différente + text_html: Or, %{link} + session_timedout: Nous vous avons déconnecté. Pour votre sécurité, %{app} désactive + votre session lorsque vous demeurez sur une page sans vous déplacer pendant %{minutes} + minutes. diff --git a/config/locales/openid_connect/en.yml b/config/locales/openid_connect/en.yml index 760dd62e9dc..8b81ddee839 100644 --- a/config/locales/openid_connect/en.yml +++ b/config/locales/openid_connect/en.yml @@ -1,3 +1,4 @@ +--- en: openid_connect: authorization: @@ -7,17 +8,19 @@ en: no_valid_scope: No valid scope values found redirect_uri_invalid: redirect_uri is invalid redirect_uri_no_match: redirect_uri does not match registered redirect_uri + logout: + errors: + id_token_hint: id_token_hint was not recognized token: errors: invalid_aud: Invalid audience claim, expected %{url} - invalid_authentication: Client must authenticate via PKCE or private_key_jwt, missing either code_challenge or client_assertion + invalid_authentication: Client must authenticate via PKCE or private_key_jwt, + missing either code_challenge or client_assertion invalid_code: invalid code invalid_code_verifier: code_verifier did not match code_challenge user_info: errors: - no_authorization: No Authorization header provided malformed_authorization: Malformed Authorization header - not_found: Could not find authorization for the contents of the provided access_token or it may have expired - logout: - errors: - id_token_hint: id_token_hint was not recognized + no_authorization: No Authorization header provided + not_found: Could not find authorization for the contents of the provided access_token + or it may have expired diff --git a/config/locales/openid_connect/es.yml b/config/locales/openid_connect/es.yml index b49da324a96..2ae118aa458 100644 --- a/config/locales/openid_connect/es.yml +++ b/config/locales/openid_connect/es.yml @@ -1,23 +1,26 @@ +--- es: openid_connect: authorization: errors: - bad_client_id: NOT TRANSLATED YET - no_valid_acr_values: NOT TRANSLATED YET - no_valid_scope: NOT TRANSLATED YET - redirect_uri_invalid: NOT TRANSLATED YET - redirect_uri_no_match: NOT TRANSLATED YET - token: - errors: - invalid_aud: NOT TRANSLATED YET - invalid_authentication: NOT TRANSLATED YET - invalid_code: NOT TRANSLATED YET - invalid_code_verifier: NOT TRANSLATED YET + bad_client_id: Bad client_id + no_valid_acr_values: acr_valores encontrados no aceptables + no_valid_scope: No se han encontrado valores de magnitud válidos + redirect_uri_invalid: Redirect_uri no es válido + redirect_uri_no_match: Redirect_uri no coincide con redirect_uri registrado logout: errors: - id_token_hint: NOT TRANSLATED YET + id_token_hint: Id_token_hint no fue reconocido + token: + errors: + invalid_aud: Solicitud de audiencia no válida, esperada % {url} + invalid_authentication: El cliente debe autenticarse a través de PKCE o private_key_jwt, + faltando code_challenge o client_assertion + invalid_code: código inválido + invalid_code_verifier: code_verifier no coincide con code_challenge user_info: errors: - no_authorization: NOT TRANSLATED YET - malformed_authorization: NOT TRANSLATED YET - not_found: NOT TRANSLATED YET + malformed_authorization: Título de autorización mal formado + no_authorization: No se ha proporcionado título de autorización + not_found: No se pudo encontrar la autorización para el contenido del access_token + proporcionado o puede haber caducado diff --git a/config/locales/openid_connect/fr.yml b/config/locales/openid_connect/fr.yml new file mode 100644 index 00000000000..8c07af7b1ad --- /dev/null +++ b/config/locales/openid_connect/fr.yml @@ -0,0 +1,26 @@ +--- +fr: + openid_connect: + authorization: + errors: + bad_client_id: Mauvaise client_id + no_valid_acr_values: Valeurs acr_values inacceptables trouvées + no_valid_scope: Aucune étendue de données valide trouvée + redirect_uri_invalid: redirect_uri est non valide + redirect_uri_no_match: redirect_uri ne correspond pas au redirect_uri enregistré + logout: + errors: + id_token_hint: id_token_hint n'a pas été reconnu + token: + errors: + invalid_aud: Affirmation liée à l'auditoire non valide, attendu %{url} + invalid_authentication: Le client doit s'authentifier par PKCE ou private_key_jwt, + code_challenge ou client_assertion manquant + invalid_code: code non valide + invalid_code_verifier: code_verifier ne correspondait pas à code_challenge + user_info: + errors: + malformed_authorization: Forme de l'en-tête d'autorisation non valide + no_authorization: Aucune en-tête d'autorisation fournie + not_found: L'autorisation pour le contenu du access_token fourni introuvable + ou il peut être expiré diff --git a/config/locales/pages/es.yml b/config/locales/pages/es.yml index 19aa9760f4b..8f6b9067af5 100644 --- a/config/locales/pages/es.yml +++ b/config/locales/pages/es.yml @@ -2,5 +2,5 @@ es: pages: page_not_found: - body: NOT TRANSLATED YET - header: NOT TRANSLATED YET + body: Es posible que desee comprobar su enlace e intentar nuevamente. (404) + header: La página que buscaba no existe. diff --git a/config/locales/pages/fr.yml b/config/locales/pages/fr.yml new file mode 100644 index 00000000000..15529918a62 --- /dev/null +++ b/config/locales/pages/fr.yml @@ -0,0 +1,6 @@ +--- +fr: + pages: + page_not_found: + body: Vous pouvez revérifier votre lien et essayer de nouveau. (404) + header: La page que vous recherchez n'existe pas. diff --git a/config/locales/saml_idp/en.yml b/config/locales/saml_idp/en.yml index 571661bdb8c..48733288731 100644 --- a/config/locales/saml_idp/en.yml +++ b/config/locales/saml_idp/en.yml @@ -1,10 +1,9 @@ +--- en: saml_idp: shared: saml_post_binding: heading: Submit to continue - no_js: > - JavaScript seems to be turned off in your browser. Normally this - step happens automatically, but because you have JavaScript - turned off, please click the submit button to continue signing - in or signing out. + no_js: JavaScript seems to be turned off in your browser. Normally this step + happens automatically, but because you have JavaScript turned off, please + click the submit button to continue signing in or signing out. diff --git a/config/locales/saml_idp/es.yml b/config/locales/saml_idp/es.yml index ba48c1c2811..d8bb2e1156c 100644 --- a/config/locales/saml_idp/es.yml +++ b/config/locales/saml_idp/es.yml @@ -1,10 +1,9 @@ +--- es: saml_idp: shared: saml_post_binding: heading: Enviar para continuar - no_js: > - JavaScript parece estar desactivado en su navegador. Normalmente esto - paso se produce automáticamente, pero porque usted tiene JavaScript - desactivado, haga clic en el botón enviar para continuar iniciando o - saliendo la sesión. + no_js: JavaScript parece estar desactivado en su navegador. Normalmente, este + paso se realiza automáticamente, pero debido a que tiene JavaScript desactivado, + haga clic en el botón Enviar para continuar iniciando o cerrando la sesión. diff --git a/config/locales/saml_idp/fr.yml b/config/locales/saml_idp/fr.yml new file mode 100644 index 00000000000..89e20d33fe0 --- /dev/null +++ b/config/locales/saml_idp/fr.yml @@ -0,0 +1,10 @@ +--- +fr: + saml_idp: + shared: + saml_post_binding: + heading: Soumettre pour continuer + no_js: JavaScript semble être désactivé dans votre navigateur. Habituellement, + cette étape se déroule automatiquement, mais parce que vous avez désactivé + le JavaScript, veuillez cliquer sur le lien « soumettre » pour continuer + ou pour vous déconnecter. diff --git a/config/locales/shared/es.yml b/config/locales/shared/es.yml index b9776c4e4de..dbbb99d678f 100644 --- a/config/locales/shared/es.yml +++ b/config/locales/shared/es.yml @@ -2,6 +2,6 @@ es: shared: footer_lite: - gsa: Administración de Servicios Generales de EE. UU. + gsa: Administración General de Servicios de EE. UU. usa_banner: - official_site: Un sitio web oficial del gobierno de los Estados Unidos + official_site: Un sitio oficial del Gobierno de Estados Unidos diff --git a/config/locales/shared/fr.yml b/config/locales/shared/fr.yml new file mode 100644 index 00000000000..d54177fd046 --- /dev/null +++ b/config/locales/shared/fr.yml @@ -0,0 +1,7 @@ +--- +fr: + shared: + footer_lite: + gsa: Administration des services généraux des États-Unis + usa_banner: + official_site: Un site web officiel du gouvernement des États-Unis diff --git a/config/locales/sign_up/en.yml b/config/locales/sign_up/en.yml index 531a247d59f..413f1e3f8e9 100644 --- a/config/locales/sign_up/en.yml +++ b/config/locales/sign_up/en.yml @@ -1,16 +1,16 @@ --- - en: - sign_up: - buttons: - continue: Continue account creation - cancel: Cancel and delete your information - cancel: - modal_header: Are you sure you want to cancel? - warning_header: If you cancel now - success: Your account has been deleted. We did not save your information. - warning_points: - - You won’t have a login.gov account - - We won't keep a record of your email address, password, and phone number - - You won't be able to securely access your information using login.gov - registrations: - create_account: Create an account +en: + sign_up: + buttons: + cancel: Cancel and delete your information + continue: Continue account creation + cancel: + modal_header: Are you sure you want to cancel? + success: Your account has been deleted. We did not save your information. + warning_header: If you cancel now + warning_points: + - You won’t have a login.gov account + - We won't keep a record of your email address, password, and phone number + - You won't be able to securely access your information using login.gov + registrations: + create_account: Create an account diff --git a/config/locales/sign_up/es.yml b/config/locales/sign_up/es.yml index 48d40455ef4..a5f68f42ed0 100644 --- a/config/locales/sign_up/es.yml +++ b/config/locales/sign_up/es.yml @@ -1,14 +1,16 @@ --- - es: - sign_up: - buttons: - continue: NOT TRANSLATED YET - cancel: NOT TRANSLATED YET - cancel: - modal_header: NOT TRANSLATED YET - warning_header: NOT TRANSLATED YET - success: NOT TRANSLATED YET - warning_points: - - NOT TRANSLATED YET - registrations: - create_account: Empezar +es: + sign_up: + buttons: + cancel: Cancelar y borrar su información + continue: Continuar la creación de cuenta + cancel: + modal_header: "¿Está seguro de que desea cancelar?" + success: Su cuenta ha sido eliminada. No guardamos su información. + warning_header: Si cancela ahora + warning_points: + - No tendrá una cuenta de login.gov + - No guardaremos un registro de su email, contraseña y número de teléfono + - No podrá acceder de forma segura a su información a través de login.gov + registrations: + create_account: Crear una cuenta diff --git a/config/locales/sign_up/fr.yml b/config/locales/sign_up/fr.yml new file mode 100644 index 00000000000..7a29236c0c9 --- /dev/null +++ b/config/locales/sign_up/fr.yml @@ -0,0 +1,18 @@ +--- +fr: + sign_up: + buttons: + cancel: Annuler et supprimer votre information + continue: Continuer la création du compte + cancel: + modal_header: Souhaitez-vous vraiment annuler? + success: Le dossier contenant votre information a été effacé + warning_header: Si vous annulez maintenant + warning_points: + - Vous n'aurez pas de compte login.gov + - Nous ne conserverons pas de dossier contenant votre adresse courriel, votre + mot de passe et votre numéro de téléphone + - Vous ne serez pas en mesure d'accéder à votre information de façon sécuritaire + en utilisant login.gov + registrations: + create_account: Créer un compte diff --git a/config/locales/simple_form/en.yml b/config/locales/simple_form/en.yml index d1545816421..2b526a65b12 100644 --- a/config/locales/simple_form/en.yml +++ b/config/locales/simple_form/en.yml @@ -5,7 +5,7 @@ en: default_message: 'Please review the problems below:' 'no': 'No' required: - html: ' ' + html: mark: "*" text: This field is required 'yes': 'Yes' diff --git a/config/locales/simple_form/es.yml b/config/locales/simple_form/es.yml index 7353ddc3978..8874b4a5498 100644 --- a/config/locales/simple_form/es.yml +++ b/config/locales/simple_form/es.yml @@ -5,7 +5,7 @@ es: default_message: 'Por favor revise los siguientes problemas:' 'no': 'No' required: - html: ' ' + html: mark: "*" text: Este campo es requerido - 'yes': 'Sí' + 'yes': Sí diff --git a/config/locales/simple_form/fr.yml b/config/locales/simple_form/fr.yml new file mode 100644 index 00000000000..ed255646c2d --- /dev/null +++ b/config/locales/simple_form/fr.yml @@ -0,0 +1,11 @@ +--- +fr: + simple_form: + error_notification: + default_message: 'Veuillez examiner les problèmes ci-dessous :' + 'no': Non + required: + html: + mark: "*" + text: Ce champ est requis + 'yes': Oui diff --git a/config/locales/time/fr.yml b/config/locales/time/fr.yml new file mode 100644 index 00000000000..32b0b06bebf --- /dev/null +++ b/config/locales/time/fr.yml @@ -0,0 +1,5 @@ +--- +fr: + time: + formats: + event_timestamp: "%B %e, %Y à %-l:%M %p" diff --git a/config/locales/titles/en.yml b/config/locales/titles/en.yml index e723b867945..3a8307ef171 100644 --- a/config/locales/titles/en.yml +++ b/config/locales/titles/en.yml @@ -1,6 +1,7 @@ --- en: titles: + account: Account account_locked: Account locked confirmations: new: Resend confirmation instructions for your account @@ -14,9 +15,8 @@ en: change: Change the password for your account confirm: Confirm the password for your account forgot: Reset the password for your account - account: Account - reactivate_account: Reactivate your account personal_key: Just in case + reactivate_account: Reactivate your account registrations: new: Sign up for a account start: Get started diff --git a/config/locales/titles/es.yml b/config/locales/titles/es.yml index 3808dc22116..982903913fa 100644 --- a/config/locales/titles/es.yml +++ b/config/locales/titles/es.yml @@ -1,33 +1,33 @@ --- es: titles: - account_locked: NOT TRANSLATED YET + account: Cuenta + account_locked: Cuenta bloqueada confirmations: - new: NOT TRANSLATED YET - show: NOT TRANSLATED YET + new: Reenviar instrucciones de confirmación de su cuenta + show: Elija una contraseña edit_info: - email: NOT TRANSLATED YET - password: NOT TRANSLATED YET - phone: NOT TRANSLATED YET - enter_2fa_code: NOT TRANSLATED YET + email: Edite su email + password: Edite su contraseña + phone: Edite su número de teléfono + enter_2fa_code: Ingese su código de seguridad de sólo un uso passwords: - change: NOT TRANSLATED YET - confirm: NOT TRANSLATED YET - forgot: NOT TRANSLATED YET - account: NOT TRANSLATED YET - reactivate_account: NOT TRANSLATED YET - personal_key: NOT TRANSLATED YET + change: Cambie la contraseña de su cuenta + confirm: Confirme la contraseña de su cuenta + forgot: Restablezca la contraseña de su cuenta + personal_key: Por si acaso + reactivate_account: Reactive su cuenta registrations: - new: NOT TRANSLATED YET - start: NOT TRANSLATED YET + new: Regístrese para una cuenta + start: Empezar sign_up: - completion_html: You have %{accent} with %{app} - loa1: created your account - loa3: verified your identity + completion_html: Tiene %{accent} con %{app} + loa1: creó su cuenta + loa3: verificó su identidad totp_setup: - new: NOT TRANSLATED YET - two_factor_setup: NOT TRANSLATED YET - verify_email: NOT TRANSLATED YET - verify_profile: NOT TRANSLATED YET + new: Configure la autenticación de dos factores + two_factor_setup: Configuración de autenticación de dos factores + verify_email: Revise su email + verify_profile: Active su cuenta visitors: - index: NOT TRANSLATED YET + index: Bienvenido/a diff --git a/config/locales/titles/fr.yml b/config/locales/titles/fr.yml new file mode 100644 index 00000000000..1b16899bd5c --- /dev/null +++ b/config/locales/titles/fr.yml @@ -0,0 +1,33 @@ +--- +fr: + titles: + account: Compte + account_locked: Compte verrouillé + confirmations: + new: Envoyer les instructions de confirmation pour votre compte + show: Choisissez un mot de passe + edit_info: + email: Modifier votre adresse courriel + password: Modifier votre mot de passe + phone: Modifier votre numéro de téléphone + enter_2fa_code: Entrez le code de sécurité à utilisation unique + passwords: + change: Changez le mot de passe de votre compte + confirm: Confirmez le mot de passe de votre compte + forgot: Réinitialisez le mot de passe de votre compte + personal_key: Juste au cas + reactivate_account: Réactiver le profil + registrations: + new: S'inscrire et créer un compte + start: Démarrer + sign_up: + completion_html: Vous avez %{accent} avec %{app} + loa1: créé votre compte + loa3: verifié votre identité + totp_setup: + new: Configurer l'authentification à deux facteurs + two_factor_setup: Configuration de l'authentification à deux facteurs + verify_email: Consultez vos courriels + verify_profile: Activez votre compte + visitors: + index: Bienvenue diff --git a/config/locales/tooltips/en.yml b/config/locales/tooltips/en.yml index 55214520320..ff5023eb534 100644 --- a/config/locales/tooltips/en.yml +++ b/config/locales/tooltips/en.yml @@ -1,9 +1,8 @@ --- en: tooltips: - ssn: > - We ask for your Social Security number to help prove your identity. Some + authentication_app: An authentication application is a mobile security app that + generates security codes even if you don't have an Internet connection or cellular + service. + ssn: We ask for your Social Security number to help prove your identity. Some agencies we work with may need it to access your records. - authentication_app: An authentication application is a mobile security app - that generates security codes even if you don't have an Internet connection - or cellular service. diff --git a/config/locales/tooltips/es.yml b/config/locales/tooltips/es.yml index a691fe14468..f5fdf0e4430 100644 --- a/config/locales/tooltips/es.yml +++ b/config/locales/tooltips/es.yml @@ -1,5 +1,9 @@ --- es: tooltips: - ssn: NOT TRANSLATED YET - authentication_app: NOT TRANSLATED YET + authentication_app: Una app de autenticación es una app de seguridad móvil que + genera códigos de seguridad, incluso si no tiene una conexión de internet o + un servicio celular. + ssn: Le pedimos su número de Seguro Social para ayudar a demostrar su identidad. + Algunas agencias con las que trabajamos pueden necesitarlo para acceder a sus + registros. diff --git a/config/locales/tooltips/fr.yml b/config/locales/tooltips/fr.yml new file mode 100644 index 00000000000..6ce09fd1f9e --- /dev/null +++ b/config/locales/tooltips/fr.yml @@ -0,0 +1,9 @@ +--- +fr: + tooltips: + authentication_app: Une application d'authentification est une application de + sécurité mobile qui génère des codes de sécurité même si vous n'avez pas de + connexion internet ou de service cellulaire. + ssn: Nous vous demandons votre numéro de sécurité sociale pour nous aider à prouver + votre identité. Certaines des agences avec lesquelles nous collaborons pourraient + devoir accéder à votre dossier. diff --git a/config/locales/user_mailer/en.yml b/config/locales/user_mailer/en.yml index c3804be8068..ce320da7e90 100644 --- a/config/locales/user_mailer/en.yml +++ b/config/locales/user_mailer/en.yml @@ -1,31 +1,27 @@ --- en: user_mailer: - help_link_text: Help Center contact_link_text: contact us email_changed: - help: > - If you did not want to change your email address, please visit the %{app} + help: If you did not want to change your email address, please visit the %{app} %{help_link} or %{contact_link}. intro: The email address for your %{app} account has been changed. + help_link_text: Help Center password_changed: - help: > - If you did not make this change, please visit the %{app} - %{help_link} or %{contact_link}. + help: If you did not make this change, please visit the %{app} %{help_link} + or %{contact_link}. intro: You have a new password for your %{app} account. phone_changed: - help: > - If you did not want to change your phone number, please visit the %{app} %{help_link} - or %{contact_link}. + help: If you did not want to change your phone number, please visit the %{app} + %{help_link} or %{contact_link}. intro: The phone number associated with your %{app} account has been changed. subject: New phone number signup_with_your_email: - help: > - If you did not request a new account or suspect an error, please visit + help: If you did not request a new account or suspect an error, please visit the %{app} %{help_link} or %{contact_link}. - intro: > - This email address is already associated with a %{app} account, so we can't use it to - create a new account. To sign in with your existing account, follow the link below. - If you are not trying to sign in with this email address, you can ignore this message. + intro: This email address is already associated with a %{app} account, so we + can't use it to create a new account. To sign in with your existing account, + follow the link below. If you are not trying to sign in with this email address, + you can ignore this message. link_text: Go to %{app} reset_password: If you can't remember your password, go to %{app} to reset it. diff --git a/config/locales/user_mailer/es.yml b/config/locales/user_mailer/es.yml index 781dc005117..56d369a3bf1 100644 --- a/config/locales/user_mailer/es.yml +++ b/config/locales/user_mailer/es.yml @@ -1,20 +1,25 @@ --- es: user_mailer: - help_link_text: NOT TRANSLATED YET - contact_link_text: NOT TRANSLATED YET + contact_link_text: Contáctenos email_changed: - help: NOT TRANSLATED YET - intro: NOT TRANSLATED YET + help: Si no desea cambiar su email, visite el %{app} %{help_link} o el %{contact_link}. + intro: El email de su cuenta de {app} ha sido cambiado. + help_link_text: Centro de Ayuda password_changed: - help: NOT TRANSLATED YET - intro: NOT TRANSLATED YET + help: Si no realizó este cambio, visite el %{app} % {help_link} o el %{contact_link}. + intro: Tiene una contraseña nueva para su cuenta de %{app}. phone_changed: - help: NOT TRANSLATED YET - intro: NOT TRANSLATED YET - subject: NOT TRANSLATED YET + help: Si no desea cambiar su número de teléfono, visite el %{app} %{help_link} + o el %{contact_link}. + intro: El número de teléfono asociado a su cuenta de %{app} ha sido cambiado. + subject: Nuevo número de teléfono signup_with_your_email: - help: NOT TRANSLATED YET - intro: NOT TRANSLATED YET - link_text: NOT TRANSLATED YET - reset_password: NOT TRANSLATED YET + help: Si no solicitó una cuenta nueva o sospecha un error, visite el %{app} + %{help_link} o el %{contact_link}. + intro: Este email ya está asociado a una cuenta %{app}, por lo tanto no podemos + usarlo para crear una cuenta nueva. Para iniciar una sesión con su cuenta + existente, siga el siguiente enlace. Si no intenta iniciar una sesión con + este email, puede ignorar este mensaje. + link_text: Ir a %{app} + reset_password: Si no recuerda su contraseña, vaya a %{app} para restablecerla. diff --git a/config/locales/user_mailer/fr.yml b/config/locales/user_mailer/fr.yml new file mode 100644 index 00000000000..44da5ec3601 --- /dev/null +++ b/config/locales/user_mailer/fr.yml @@ -0,0 +1,28 @@ +--- +fr: + user_mailer: + contact_link_text: communiquez avec nous + email_changed: + help: Si vous préférez ne pas changer votre adresse courriel, veuillez visiter + le %{help_link} de %{app} ou %{contact_link}. + intro: L'adresse courriel de votre compte %{app} a été changée. + help_link_text: Centre d'aide + password_changed: + help: Si vous n'avez pas changé votre mot de passe, veuillez visiter le %{help_link} + de %{app} ou %{contact_link}. + intro: Le mot de passe de votre compte %{app} a été changé. + phone_changed: + help: Si vous ne souhaitiez pas changer votre numéro de téléphone, veuillez + visiter le %{help_link} de %{app} ou %{contact_link}. + intro: Le numéro de téléphone associé à votre compte %{app} a été changé. + subject: Nouveau numéro de téléphone + signup_with_your_email: + help: Si vous n'avez pas demandé un nouveau compte ou que vous soupçonnez qu'une + erreur s'est produite, veuillez visiter le %{help_link} de %{app} ou %{contact_link}. + intro: Cette adresse courriel est déjà associée à un compte %{app}, nous ne + pouvons donc pas l'utiliser pour créer un nouveau compte. Pour vous connecter + à votre compte existant, suivez le lien ci-dessous. Si vous ne tentez pas + de vous connecter avec cette adresse courriel, vous pouvez ignorer ce message. + link_text: Allez à %{app} + reset_password: Si vous ne vous souvenez plus de votre mot de passe, allez à + %{app} pour le réinitialiser. diff --git a/config/locales/users/en.yml b/config/locales/users/en.yml index 4c931203869..cfe8561dda7 100644 --- a/config/locales/users/en.yml +++ b/config/locales/users/en.yml @@ -1,16 +1,13 @@ --- en: users: - totp_setup: - new: - qr_img_alt: QR Code for Authenticator App personal_key: - header: Your personal key + close: Close + confirmation_error: You've entered an incorrect personal key. generated_on_html: Generated on %{date} get_another: Get another key - print: Print this page - help_text_header: Why do I need to store my new key on paper? - help_text: | + header: Your personal key + help_text: |- To protect your account, you need a password and access to your telephone or authentication application at sign-in. If you can’t use your phone or app, you can sign in with your personal key instead. For your privacy and security, login.gov does not store your password and personal key. Only you know them. Only you can access or share your personal information. @@ -18,5 +15,8 @@ en: We require you to store your personal key outside your computer or mobile device so that it will be safe even if your devices are stolen or your online accounts are hacked. If you don’t have your personal key and you forget your password, the only way to keep your account safe is to verify that you are the legal owner. - close: Close - confirmation_error: You've entered an incorrect personal key. + help_text_header: Why do I need to store my new key on paper? + print: Print this page + totp_setup: + new: + qr_img_alt: QR Code for Authenticator App diff --git a/config/locales/users/es.yml b/config/locales/users/es.yml index 65fc4f391cf..543d5d8d4bf 100644 --- a/config/locales/users/es.yml +++ b/config/locales/users/es.yml @@ -1,14 +1,22 @@ +--- es: users: + personal_key: + close: Cerrar + confirmation_error: Ha ingresado una clave personal incorrecta. + generated_on_html: Generado el %{date} + get_another: Obtener otra clave + header: Su clave personal + help_text: |- + Para proteger su cuenta, necesita una contraseña y acceso a su teléfono o app de autenticación al iniciar una sesión. Si no puede utilizar su teléfono o app, puede iniciar una sesión con su clave personal. + + Para su privacidad y seguridad, login.gov no guarda su contraseña y clave personal. Sólo usted las conoce. Sólo usted puede acceder o compartir su información personal. + + Le pedimos que guarde su clave personal afuera de su computadora o dispositivo móvil para que mantenerla segura en caso de que le roben sus aparatos o sus cuentas en línea sean hackeadas. + + Si no tiene su clave personal y olvida su contraseña, la única manera de mantener su cuenta segura es verificando que usted es el propietario legal. + help_text_header: "¿Por qué necesito guardar mi nueva clave en papel?" + print: Imprima esta página totp_setup: new: - qr_img_alt: Código de QR para la aplicación de autenticación - personal_key: - header: NOT TRANSLATED YET - generated_on_html: NO TRANSLATED YET - get_another: NOT TRANSLATED YET - print: NOT TRANSLATED YET - help_text_header: NOT TRANSLATED YET - help_text: NOT TRANSLATED YET - close: NOT TRANSLATED YET - confirmation_error: NOT TRANSLATED YET + qr_img_alt: Código de QR para la app de autenticación diff --git a/config/locales/users/fr.yml b/config/locales/users/fr.yml new file mode 100644 index 00000000000..27baff23a54 --- /dev/null +++ b/config/locales/users/fr.yml @@ -0,0 +1,22 @@ +--- +fr: + users: + personal_key: + close: Fermer + confirmation_error: Vous avez entré un clé personnelle erronée. + generated_on_html: Générée le %{date} + get_another: Obtenir une autre clé + header: Votre clé personnelle + help_text: |- + Pour protéger votre compte, vous devez avoir un mot de passe et l'accès à votre téléphone ou application d'authentification au moment de la connexion. Si vous ne pouvez utiliser votre téléphone ou application, vous pouvez vous connecter avec votre clé personnelle. + + Pour votre confidentialité et votre sécurité, login.gov ne conserve pas votre mot de passe ni votre clé personnelle. Seul(e) vous les connaissez. Seul(e) vous pouvez accéder à votre information personnelle et la partager . + + Nous vous demandons de conserver votre clé personnelle à l'extérieur de votre ordinateur ou appareil mobile afin qu'elle soit en sûreté même si vos appareils sont volés ou si vos comptes en ligne sont piratés. + + Si vous n'avez pas votre clé personnelle et que vous oubliez votre mot de passe, la seule façon de garder votre compte en sécurité est de vérifier que vous en êtes le(la) propriétaire légal(e). + help_text_header: Pourquoi dois-je conserver ma nouvelle clé sur papier? + print: Imprimer cette page + totp_setup: + new: + qr_img_alt: Code QR pour l'application d'authentification diff --git a/config/locales/valid_email/en.yml b/config/locales/valid_email/en.yml index 2e106d820ac..b678f98820a 100644 --- a/config/locales/valid_email/en.yml +++ b/config/locales/valid_email/en.yml @@ -1,5 +1,7 @@ +--- en: valid_email: validations: email: - invalid: Invalid email address format or domain entered. Correct the address and re-enter it. + invalid: Invalid email address format or domain entered. Correct the address + and re-enter it. diff --git a/config/locales/valid_email/es.yml b/config/locales/valid_email/es.yml index 3ead9a91c9e..7fc94c3cab7 100644 --- a/config/locales/valid_email/es.yml +++ b/config/locales/valid_email/es.yml @@ -1,5 +1,7 @@ +--- es: valid_email: validations: email: - invalid: El formato de dirección de correo electrónico o el dominio ingresado no es válido. Corrija la dirección y vuelva a ingresarla. + invalid: El formato de email o dominio ingresado no es válido. Corrija su + email y vuelva a intentarlo. diff --git a/config/locales/valid_email/fr.yml b/config/locales/valid_email/fr.yml new file mode 100644 index 00000000000..2036952ded5 --- /dev/null +++ b/config/locales/valid_email/fr.yml @@ -0,0 +1,7 @@ +--- +fr: + valid_email: + validations: + email: + invalid: Format d'adresse courriel ou domaine entré non valide. Corrigez l'adresse + et entrez-la de nouveau. diff --git a/config/locales/zxcvbn/en.yml b/config/locales/zxcvbn/en.yml index 05e228179f1..2e7fa4794bb 100644 --- a/config/locales/zxcvbn/en.yml +++ b/config/locales/zxcvbn/en.yml @@ -2,39 +2,46 @@ en: zxcvbn: feedback: - "A word by itself is easy to guess": A word by itself is easy to guess - "Add another word or two_ Uncommon words are better_": >- - Add another word or two. Uncommon words are better - "All-uppercase is almost as easy to guess as all-lowercase": >- - All-uppercase is almost as easy to guess as all-lowercase - "Avoid dates and years that are associated with you": >- - Avoid dates and years that are associated with you - "Avoid recent years": Avoid recent years - "Avoid repeated words and characters": Avoid repeated words and characters - "Avoid sequences": Avoid sequences - "Avoid years that are associated with you": Avoid years that are associated with you - "Capitalization doesn't help very much": Capitalization doesn’t help very much - "Common names and surnames are easy to guess": Common names and surnames are easy to guess - "Dates are often easy to guess": Dates are often easy to guess - "Names and surnames by themselves are easy to guess": >- - Names and surnames by themselves are easy to guess - "No need for symbols, digits, or uppercase letters": >- - There is no need for symbols, digits, or uppercase letters - "Predictable substitutions like '@' instead of 'a' don't help very much": - Predictable substitutions like '@' instead of 'a' don’t help very much - "Recent years are easy to guess": Recent years are easy to guess - "Repeats like \"aaa\" are easy to guess": Repeats like "aaa" are easy to guess - "Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\"": >- - Repeats like "abcabcabc" are only slightly harder to guess than "abc" - "Reversed words aren't much harder to guess": Reversed words aren’t much harder to guess - "Sequences like abc or 6543 are easy to guess": Sequences like abc or 6543 are easy to guess - "Short keyboard patterns are easy to guess": Short keyboard patterns are easy to guess - "Straight rows of keys are easy to guess": Straight rows of keys are easy to guess - "This is a top-10 common password": This is a top-10 common password - "This is a top-100 common password": This is a top-100 common password - "This is a very common password": This is a very common password - "This is similar to a commonly used password": This is similar to a commonly used password - "Use a few words, avoid common phrases": - For a stronger password, use a few words separated by spaces, but avoid common phrases - "Use a longer keyboard pattern with more turns": >- - Use a longer keyboard pattern with more turns + a_word_by_itself_is_easy_to_guess: A word by itself is easy to guess + add_another_word_or_two_uncommon_words_are_better: Add another word or two. + Uncommon words are better + all_uppercase_is_almost_as_easy_to_guess_as_all_lowercase: All-uppercase is + almost as easy to guess as all-lowercase + avoid_dates_and_years_that_are_associated_with_you: Avoid dates and years that + are associated with you + avoid_recent_years: Avoid recent years + avoid_repeated_words_and_characters: Avoid repeated words and characters + avoid_sequences: Avoid sequences + avoid_years_that_are_associated_with_you: Avoid years that are associated with + you + capitalization_doesnt_help_very_much: Capitalization doesn’t help very much + common_names_and_surnames_are_easy_to_guess: Common names and surnames are easy + to guess + dates_are_often_easy_to_guess: Dates are often easy to guess + for_a_stronger_password_use_a_few_words_separated_by_spaces_but_avoid_common_phrases: For + a stronger password, use a few words separated by spaces, but avoid common + phrases + names_and_surnames_by_themselves_are_easy_to_guess: Names and surnames by themselves + are easy to guess + predictable_substitutions_like__instead_of_a_dont_help_very_much: Predictable + substitutions like '@' instead of 'a' don’t help very much + recent_years_are_easy_to_guess: Recent years are easy to guess + repeats_like_aaa_are_easy_to_guess: Repeats like "aaa" are easy to guess + repeats_like_abcabcabc_are_only_slightly_harder_to_guess_than_abc: Repeats like + "abcabcabc" are only slightly harder to guess than "abc" + reversed_words_arent_much_harder_to_guess: Reversed words aren’t much harder + to guess + sequences_like_abc_or_6543_are_easy_to_guess: Sequences like abc or 6543 are + easy to guess + short_keyboard_patterns_are_easy_to_guess: Short keyboard patterns are easy + to guess + straight_rows_of_keys_are_easy_to_guess: Straight rows of keys are easy to guess + there_is_no_need_for_symbols_digits_or_uppercase_letters: There is no need for + symbols, digits, or uppercase letters + this_is_a_top_100_common_password: This is a top-100 common password + this_is_a_top_10_common_password: This is a top-10 common password + this_is_a_very_common_password: This is a very common password + this_is_similar_to_a_commonly_used_password: This is similar to a commonly used + password + use_a_longer_keyboard_pattern_with_more_turns: Use a longer keyboard pattern + with more turns diff --git a/config/locales/zxcvbn/es.yml b/config/locales/zxcvbn/es.yml index 086ffa24261..268dced5c3d 100644 --- a/config/locales/zxcvbn/es.yml +++ b/config/locales/zxcvbn/es.yml @@ -2,30 +2,48 @@ es: zxcvbn: feedback: - "A word by itself is easy to guess": NOT TRANSLATED YET - "Add another word or two_ Uncommon words are better_": NOT TRANSLATED YET - "All-uppercase is almost as easy to guess as all-lowercase": NOT TRANSLATED YET - "Avoid dates and years that are associated with you": NOT TRANSLATED YET - "Avoid recent years": NOT TRANSLATED YET - "Avoid repeated words and characters": NOT TRANSLATED YET - "Avoid sequences": NOT TRANSLATED YET - "Avoid years that are associated with you": NOT TRANSLATED YET - "Capitalization doesn't help very much": NOT TRANSLATED YET - "Common names and surnames are easy to guess": NOT TRANSLATED YET - "Dates are often easy to guess": NOT TRANSLATED YET - "Names and surnames by themselves are easy to guess": NOT TRANSLATED YET - "No need for symbols, digits, or uppercase letters": NOT TRANSLATED YET - "Predictable substitutions like '@' instead of 'a' don't help very much": NOT TRANSLATED YET - "Recent years are easy to guess": NOT TRANSLATED YET - "Repeats like \"aaa\" are easy to guess": NOT TRANSLATED YET - "Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\"": NOT TRANSLATED YET - "Reversed words aren't much harder to guess": NOT TRANSLATED YET - "Sequences like abc or 6543 are easy to guess": NOT TRANSLATED YET - "Short keyboard patterns are easy to guess": NOT TRANSLATED YET - "Straight rows of keys are easy to guess": NOT TRANSLATED YET - "This is a top-10 common password": NOT TRANSLATED YET - "This is a top-100 common password": NOT TRANSLATED YET - "This is a very common password": NOT TRANSLATED YET - "This is similar to a commonly used password": NOT TRANSLATED YET - "Use a few words, avoid common phrases": NOT TRANSLATED YET - "Use a longer keyboard pattern with more turns": NOT TRANSLATED YET + a_word_by_itself_is_easy_to_guess: Una sola palabra es fácil de adivinar. + add_another_word_or_two_uncommon_words_are_better: Añada otra palabra o dos. + Las palabras poco comunes son mejor opción. + all_uppercase_is_almost_as_easy_to_guess_as_all_lowercase: Todo en mayúsculas + es casi igual de fácil de adivinar como todo en minúsculas. + avoid_dates_and_years_that_are_associated_with_you: Evite las fechas y los años + que están asociados con usted + avoid_recent_years: Evite los años recientes + avoid_repeated_words_and_characters: Evite palabras y caracteres repetidos + avoid_sequences: Evite secuencias + avoid_years_that_are_associated_with_you: Evite los años que están asociados + con usted + capitalization_doesnt_help_very_much: Usar mayúsculas no ayuda mucho + common_names_and_surnames_are_easy_to_guess: Los nombres y apellidos comunes + son fáciles de adivinar + dates_are_often_easy_to_guess: Las fechas suelen ser fáciles de adivinar + for_a_stronger_password_use_a_few_words_separated_by_spaces_but_avoid_common_phrases: Para + una contraseña más segura, use pocas palabras separadas por espacios, pero + evite frases comunes + names_and_surnames_by_themselves_are_easy_to_guess: Nombres y apellidos por + si solos son fáciles de adivinar + predictable_substitutions_like__instead_of_a_dont_help_very_much: No hay necesidad + de símbolos, dígitos o letras mayúsculas + recent_years_are_easy_to_guess: Sustituciones predecibles como '@' en lugar + de 'a' no ayudan mucho + repeats_like_aaa_are_easy_to_guess: Los años recientes son fáciles de adivinar + repeats_like_abcabcabc_are_only_slightly_harder_to_guess_than_abc: Las repeticiones + como "aaa" son fáciles de adivinar + reversed_words_arent_much_harder_to_guess: Las repeticiones como "abcabcabc" + son sólo un poco más difíciles de adivinar que "abc" + sequences_like_abc_or_6543_are_easy_to_guess: Las palabras invertidas no son + mucho más difíciles de adivinar + short_keyboard_patterns_are_easy_to_guess: Las secuencias como abc o 6543 son + fáciles de adivinar + straight_rows_of_keys_are_easy_to_guess: Las combinaciones cortas de teclas + son fáciles de adivinar + there_is_no_need_for_symbols_digits_or_uppercase_letters: Las líneas seguidas + de letras son fáciles de adivinar + this_is_a_top_100_common_password: Esta es una de las 10 contraseñas más comunes. + this_is_a_top_10_common_password: Esta es una de las 100 contraseñas más comunes. + this_is_a_very_common_password: Esta es una contraseña muy común + this_is_similar_to_a_commonly_used_password: Esto es similar a una contraseña + comúnmente utilizada + use_a_longer_keyboard_pattern_with_more_turns: Use una combinación larga de + teclas con más configuraciones diff --git a/config/locales/zxcvbn/fr.yml b/config/locales/zxcvbn/fr.yml new file mode 100644 index 00000000000..3666090103b --- /dev/null +++ b/config/locales/zxcvbn/fr.yml @@ -0,0 +1,51 @@ +--- +fr: + zxcvbn: + feedback: + a_word_by_itself_is_easy_to_guess: Un mot seul est facile à deviner + add_another_word_or_two_uncommon_words_are_better: Ajoutez un ou deux autres + mots. Les mots non communs sont plus efficaces + all_uppercase_is_almost_as_easy_to_guess_as_all_lowercase: Tout en majuscules + est presque aussi facile à deviner que tout en minuscules + avoid_dates_and_years_that_are_associated_with_you: Évitez les dates et années + qui vous sont associées + avoid_recent_years: Évitez les années récentes + avoid_repeated_words_and_characters: Évitez les mots et caractères répétés + avoid_sequences: Évitez les séquences + avoid_years_that_are_associated_with_you: Évitez les années qui vous sont associées + capitalization_doesnt_help_very_much: La capitalisation n'aide pas beaucoup + common_names_and_surnames_are_easy_to_guess: Les prénoms et noms de famille + communs sont faciles à deviner + dates_are_often_easy_to_guess: Les dates sont souvent faciles à deviner + for_a_stronger_password_use_a_few_words_separated_by_spaces_but_avoid_common_phrases: Pour + créer un mot de passe plus fort, utilisez quelques mots séparés par des espaces, + mais évitez les phrases communes + names_and_surnames_by_themselves_are_easy_to_guess: Les prénoms et noms de famille + seuls sont faciles à deviner + predictable_substitutions_like__instead_of_a_dont_help_very_much: Les remplacements + prévisibles comme es « @ » au lieu de « à » n'aident pas beaucoup + recent_years_are_easy_to_guess: Les années récentes sont faciles à deviner + repeats_like_aaa_are_easy_to_guess: Les répétitions comme « aaa » sont faciles + à deviner + repeats_like_abcabcabc_are_only_slightly_harder_to_guess_than_abc: |- + Les répétitions comme « abcabcabc » sont à peine + plus difficiles à deviner que « abc » + reversed_words_arent_much_harder_to_guess: Les mots inversés ne sont pas très + difficiles à deviner + sequences_like_abc_or_6543_are_easy_to_guess: Les séquences comme abc ou 6543 + sont faciles à deviner + short_keyboard_patterns_are_easy_to_guess: Les motifs de clavier courts sont + faciles à deviner + straight_rows_of_keys_are_easy_to_guess: Les rangées de lettres consécutives + sont faciles à deviner + there_is_no_need_for_symbols_digits_or_uppercase_letters: Les symboles, les + chiffres ou les lettres majuscules ne sont pas nécessaires + this_is_a_top_100_common_password: Il s'agit d'un des 100 mots de passe les + plus communs + this_is_a_top_10_common_password: Il s'agit d'un des 10 mots de passe les plus + communs + this_is_a_very_common_password: Il s'agit d'un mot de passe très commun + this_is_similar_to_a_commonly_used_password: Ceci est similaire à un mot de + passe souvent utilisé + use_a_longer_keyboard_pattern_with_more_turns: Utilisez un motif de clavier + plus long avec plus de tours diff --git a/config/routes.rb b/config/routes.rb index a21e217810d..6565bcbd982 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,60 +2,7 @@ require 'sidekiq/web' mount Sidekiq::Web => '/sidekiq', constraints: AdminConstraint.new - # Devise handles login itself. It's first in the chain to avoid a redirect loop during - # authentication failure. - devise_for( - :users, - skip: %i[confirmations sessions registrations two_factor_authentication], - controllers: { passwords: 'users/reset_passwords' } - ) - - # Additional device controller routes. - devise_scope :user do - get '/' => 'users/sessions#new', as: :new_user_session - post '/' => 'users/sessions#create', as: :user_session - get '/active' => 'users/sessions#active' - - get '/login/two_factor/authenticator' => 'two_factor_authentication/totp_verification#show' - post '/login/two_factor/authenticator' => 'two_factor_authentication/totp_verification#create' - get '/login/two_factor/personal_key' => 'two_factor_authentication/personal_key_verification#show' - post '/login/two_factor/personal_key' => 'two_factor_authentication/personal_key_verification#create' - get '/login/two_factor/:otp_delivery_preference' => 'two_factor_authentication/otp_verification#show', - as: :login_two_factor - post '/login/two_factor/:otp_delivery_preference' => 'two_factor_authentication/otp_verification#create', - as: :login_otp - - get '/reauthn' => 'mfa_confirmation#new', as: :user_password_confirm - post '/reauthn' => 'mfa_confirmation#create', as: :reauthn_user_password - get '/timeout' => 'users/sessions#timeout' - end - - if Figaro.env.enable_test_routes == 'true' - namespace :test do - # Assertion granting test start + return. - get '/saml' => 'saml_test#start' - get '/saml/decode_assertion' => 'saml_test#start' - post '/saml/decode_assertion' => 'saml_test#decode_response' - post '/saml/decode_slo_request' => 'saml_test#decode_slo_request' - end - end - - # Non-devise-controller routes. Alphabetically sorted. - get '/.well-known/openid-configuration' => 'openid_connect/configuration#index', - as: :openid_connect_configuration - - get '/account' => 'accounts#show' - get '/account/reactivate/start' => 'reactivate_account#index', as: :reactivate_account - put '/account/reactivate/start' => 'reactivate_account#update' - get '/account/reactivate/verify_password' => 'users/verify_password#new', as: :verify_password - put '/account/reactivate/verify_password' => 'users/verify_password#update', as: :update_verify_password - get '/account/reactivate/verify_personal_key' => 'users/verify_personal_key#new', - as: :verify_personal_key - post '/account/reactivate/verify_personal_key' => 'users/verify_personal_key#create', - as: :create_verify_personal_key - get '/account/verify_phone' => 'users/verify_profile_phone#index', as: :verify_profile_phone - post '/account/verify_phone' => 'users/verify_profile_phone#create' - + # Non i18n routes. Alphabetically sorted. get '/api/health/workers' => 'health/workers#index' get '/api/openid_connect/certs' => 'openid_connect/certs#index' post '/api/openid_connect/token' => 'openid_connect/token#create' @@ -69,86 +16,146 @@ post '/api/service_provider' => 'service_provider#update' - delete '/authenticator_setup' => 'users/totp_setup#disable', as: :disable_totp - get '/authenticator_setup' => 'users/totp_setup#new' - patch '/authenticator_setup' => 'users/totp_setup#confirm' - get '/authenticator_start' => 'users/totp_setup#start' + get '/openid_connect/authorize' => 'openid_connect/authorization#index' + get '/openid_connect/logout' => 'openid_connect/logout#index' - get '/forgot_password' => 'forgot_password#show' + # i18n routes. Alphabetically sorted. + scope '(:locale)', locale: /#{I18n.available_locales.join('|')}/ do + # Devise handles login itself. It's first in the chain to avoid a redirect loop during + # authentication failure. + devise_for( + :users, + skip: %i[confirmations sessions registrations two_factor_authentication], + controllers: { passwords: 'users/reset_passwords' } + ) + + # Additional device controller routes. + devise_scope :user do + get '/' => 'users/sessions#new', as: :new_user_session + post '/' => 'users/sessions#create', as: :user_session + get '/active' => 'users/sessions#active' + + get '/login/two_factor/authenticator' => 'two_factor_authentication/totp_verification#show' + post '/login/two_factor/authenticator' => 'two_factor_authentication/totp_verification#create' + get '/login/two_factor/personal_key' => 'two_factor_authentication/personal_key_verification#show' + post '/login/two_factor/personal_key' => 'two_factor_authentication/personal_key_verification#create' + get '/login/two_factor/:otp_delivery_preference' => 'two_factor_authentication/otp_verification#show', + as: :login_two_factor + post '/login/two_factor/:otp_delivery_preference' => 'two_factor_authentication/otp_verification#create', + as: :login_otp + + get '/reauthn' => 'mfa_confirmation#new', as: :user_password_confirm + post '/reauthn' => 'mfa_confirmation#create', as: :reauthn_user_password + get '/timeout' => 'users/sessions#timeout' + end - get '/manage/email' => 'users/emails#edit' - match '/manage/email' => 'users/emails#update', via: %i[patch put] - get '/manage/password' => 'users/passwords#edit' - patch '/manage/password' => 'users/passwords#update' - get '/manage/phone' => 'users/phones#edit' - match '/manage/phone' => 'users/phones#update', via: %i[patch put] - get '/manage/personal_key' => 'users/personal_keys#show', as: :manage_personal_key - post '/manage/personal_key' => 'users/personal_keys#update' + if Figaro.env.enable_test_routes == 'true' + namespace :test do + # Assertion granting test start + return. + get '/saml' => 'saml_test#start' + get '/saml/decode_assertion' => 'saml_test#start' + post '/saml/decode_assertion' => 'saml_test#decode_response' + post '/saml/decode_slo_request' => 'saml_test#decode_slo_request' + end + end - get '/openid_connect/authorize' => 'openid_connect/authorization#index' - get '/openid_connect/logout' => 'openid_connect/logout#index' + # Non-devise-controller routes. Alphabetically sorted. + get '/.well-known/openid-configuration' => 'openid_connect/configuration#index', + as: :openid_connect_configuration + + get '/account' => 'accounts#show' + get '/account/reactivate/start' => 'reactivate_account#index', as: :reactivate_account + put '/account/reactivate/start' => 'reactivate_account#update' + get '/account/reactivate/verify_password' => 'users/verify_password#new', as: :verify_password + put '/account/reactivate/verify_password' => 'users/verify_password#update', as: :update_verify_password + get '/account/reactivate/verify_personal_key' => 'users/verify_personal_key#new', + as: :verify_personal_key + post '/account/reactivate/verify_personal_key' => 'users/verify_personal_key#create', + as: :create_verify_personal_key + get '/account/verify_phone' => 'users/verify_profile_phone#index', as: :verify_profile_phone + post '/account/verify_phone' => 'users/verify_profile_phone#create' + + delete '/authenticator_setup' => 'users/totp_setup#disable', as: :disable_totp + get '/authenticator_setup' => 'users/totp_setup#new' + patch '/authenticator_setup' => 'users/totp_setup#confirm' + get '/authenticator_start' => 'users/totp_setup#start' + + get '/forgot_password' => 'forgot_password#show' + + get '/manage/email' => 'users/emails#edit' + match '/manage/email' => 'users/emails#update', via: %i[patch put] + get '/manage/password' => 'users/passwords#edit' + patch '/manage/password' => 'users/passwords#update' + get '/manage/phone' => 'users/phones#edit' + match '/manage/phone' => 'users/phones#update', via: %i[patch put] + get '/manage/personal_key' => 'users/personal_keys#show', as: :manage_personal_key + post '/manage/personal_key' => 'users/personal_keys#update' + + get '/otp/send' => 'users/two_factor_authentication#send_code' + get '/phone_setup' => 'users/two_factor_authentication_setup#index' + patch '/phone_setup' => 'users/two_factor_authentication_setup#set' + get '/users/two_factor_authentication' => 'users/two_factor_authentication#show', + as: :user_two_factor_authentication # route name is used by two_factor_authentication gem + + get '/profile', to: redirect('/account') + get '/profile/reactivate', to: redirect('/account/reactivate') + get '/profile/verify', to: redirect('/account/verify') + + post '/sign_up/create_password' => 'sign_up/passwords#create', as: :sign_up_create_password + get '/sign_up/email/confirm' => 'sign_up/email_confirmations#create', + as: :sign_up_create_email_confirmation + get '/sign_up/enter_email' => 'sign_up/registrations#new', as: :sign_up_email + post '/sign_up/enter_email' => 'sign_up/registrations#create', as: :sign_up_register + get '/sign_up/enter_email/resend' => 'sign_up/email_resend#new', as: :sign_up_email_resend + post '/sign_up/enter_email/resend' => 'sign_up/email_resend#create', + as: :sign_up_create_email_resend + get '/sign_up/enter_password' => 'sign_up/passwords#new' + get '/sign_up/personal_key' => 'sign_up/personal_keys#show' + post '/sign_up/personal_key' => 'sign_up/personal_keys#update' + get '/sign_up/start' => 'sign_up/registrations#show', as: :sign_up_start + 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' + + match '/sign_out' => 'sign_out#destroy', via: %i[get post delete] + + delete '/users' => 'users#destroy', as: :destroy_user + + if FeatureManagement.enable_identity_verification? + get '/verify' => 'verify#index' + get '/verify/activated' => 'verify#activated' + get '/verify/address' => 'verify/address#index' + get '/verify/cancel' => 'verify#cancel' + get '/verify/confirmations' => 'verify/confirmations#show' + post '/verify/confirmations' => 'verify/confirmations#update' + get '/verify/fail' => 'verify#fail' + get '/verify/finance' => 'verify/finance#new' + put '/verify/finance' => 'verify/finance#create' + get '/verify/finance/other' => 'verify/finance_other#new' + get '/verify/finance/result' => 'verify/finance#show' + get '/verify/phone' => 'verify/phone#new' + put '/verify/phone' => 'verify/phone#create' + get '/verify/phone/result' => 'verify/phone#show' + get '/verify/review' => 'verify/review#new' + put '/verify/review' => 'verify/review#create' + get '/verify/session' => 'verify/sessions#new' + put '/verify/session' => 'verify/sessions#create' + get '/verify/session/result' => 'verify/sessions#show' + delete '/verify/session' => 'verify/sessions#destroy' + get '/verify/session/dupe' => 'verify/sessions#dupe' - get '/otp/send' => 'users/two_factor_authentication#send_code' - get '/phone_setup' => 'users/two_factor_authentication_setup#index' - patch '/phone_setup' => 'users/two_factor_authentication_setup#set' - get '/users/two_factor_authentication' => 'users/two_factor_authentication#show', - as: :user_two_factor_authentication # route name is used by two_factor_authentication gem - - get '/profile', to: redirect('/account') - get '/profile/reactivate', to: redirect('/account/reactivate') - get '/profile/verify', to: redirect('/account/verify') - - post '/sign_up/create_password' => 'sign_up/passwords#create', as: :sign_up_create_password - get '/sign_up/email/confirm' => 'sign_up/email_confirmations#create', - as: :sign_up_create_email_confirmation - get '/sign_up/enter_email' => 'sign_up/registrations#new', as: :sign_up_email - post '/sign_up/enter_email' => 'sign_up/registrations#create', as: :sign_up_register - get '/sign_up/enter_email/resend' => 'sign_up/email_resend#new', as: :sign_up_email_resend - post '/sign_up/enter_email/resend' => 'sign_up/email_resend#create', - as: :sign_up_create_email_resend - get '/sign_up/enter_password' => 'sign_up/passwords#new' - get '/sign_up/personal_key' => 'sign_up/personal_keys#show' - post '/sign_up/personal_key' => 'sign_up/personal_keys#update' - get '/sign_up/start' => 'sign_up/registrations#show', as: :sign_up_start - 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' - - match '/sign_out' => 'sign_out#destroy', via: %i[get post delete] - - delete '/users' => 'users#destroy', as: :destroy_user - - if FeatureManagement.enable_identity_verification? - get '/verify' => 'verify#index' - get '/verify/activated' => 'verify#activated' - get '/verify/address' => 'verify/address#index' - get '/verify/cancel' => 'verify#cancel' - get '/verify/confirmations' => 'verify/confirmations#show' - post '/verify/confirmations' => 'verify/confirmations#update' - get '/verify/fail' => 'verify#fail' - get '/verify/finance' => 'verify/finance#new' - put '/verify/finance' => 'verify/finance#create' - get '/verify/finance/other' => 'verify/finance_other#new' - get '/verify/phone' => 'verify/phone#new' - put '/verify/phone' => 'verify/phone#create' - get '/verify/review' => 'verify/review#new' - put '/verify/review' => 'verify/review#create' - get '/verify/session' => 'verify/sessions#new' - put '/verify/session' => 'verify/sessions#create' - delete '/verify/session' => 'verify/sessions#destroy' - get '/verify/session/dupe' => 'verify/sessions#dupe' + end - end + if FeatureManagement.enable_usps_verification? + get '/account/verify' => 'users/verify_account#index', as: :verify_account + post '/account/verify' => 'users/verify_account#create' + get '/verify/usps' => 'verify/usps#index' + put '/verify/usps' => 'verify/usps#create' + end - if FeatureManagement.enable_usps_verification? - get '/account/verify' => 'users/verify_account#index', as: :verify_account - post '/account/verify' => 'users/verify_account#create' - get '/verify/usps' => 'verify/usps#index' - put '/verify/usps' => 'verify/usps#create' + root to: 'users/sessions#new' end - root to: 'users/sessions#new' - # Make sure any new routes are added above this line! # The line below will route all requests that aren't # defined route to the 404 page. Therefore, anything you put after this rule diff --git a/config/schedule.rb b/config/schedule.rb index 7ab3c4cf593..5351f54b4e7 100644 --- a/config/schedule.rb +++ b/config/schedule.rb @@ -15,3 +15,11 @@ runner 'WorkerHealthChecker.check' runner 'WorkerHealthChecker.enqueue_dummy_jobs' end + +if FeatureManagement.enable_usps_verification? + mail_batch = Whenever.seconds(Figaro.env.usps_mail_batch_hours.to_i, :hours) + + every mail_batch, roles: [:job_creator] do + runner 'UspsUploader.new.run' + end +end diff --git a/config/service_providers.yml b/config/service_providers.yml index e61ac94bdde..199a8dc5e22 100644 --- a/config/service_providers.yml +++ b/config/service_providers.yml @@ -383,7 +383,17 @@ production: 'urn:gov:dhs.cbp.jobs:openidconnect:jenkins-pspd-credential-service': friendly_name: 'CBP PSPD Trusted Traveler Programs' agency: 'DHS' - logo: 'cbp.png' + logo: 'cbp-ttp.png' cert: 'cbp_goes_pre_prod' redirect_uris: - 'http://10.156.152.27/login' + + 'urn:gov:dhs.cbp.jobs:openidconnect:aws-cbp-ttp': + agency: 'DHS' + allow_on_prod_chef_env: 'true' + block_encryption: 'aes256-cbc' + cert: 'cbp_goes_prod' + friendly_name: 'CBP Trusted Traveler Programs' + logo: 'cbp-ttp.png' + redirect_uris: + - 'https://ttp.cbp.dhs.gov' diff --git a/config/sidekiq.yml b/config/sidekiq.yml index 8a0466b9b10..f72f4153bcd 100644 --- a/config/sidekiq.yml +++ b/config/sidekiq.yml @@ -3,4 +3,5 @@ - voice - mailers - analytics + - idv :logfile: 'log/sidekiq.log' diff --git a/lib/feature_management.rb b/lib/feature_management.rb index d8ef77b268d..e88d2e3f93e 100644 --- a/lib/feature_management.rb +++ b/lib/feature_management.rb @@ -59,4 +59,8 @@ def self.reveal_usps_code? def self.current_env_allowed_to_see_usps_code? ENVS_WHERE_PREFILLING_USPS_CODE_ALLOWED.include?(Figaro.env.domain_name) end + + def self.no_pii_mode? + enable_identity_verification? && Idv::Vendor.new.pick == :mock + end end diff --git a/lib/yaml_normalizer.rb b/lib/yaml_normalizer.rb new file mode 100644 index 00000000000..347a0421ebd --- /dev/null +++ b/lib/yaml_normalizer.rb @@ -0,0 +1,41 @@ +require 'yaml' +require 'active_support/core_ext/object/try' + +class YamlNormalizer + # Reads in YAML at a path, trims whitespace from each key, and writes it back to the file + def self.run(argv) + argv.each do |file| + $stderr.puts file + data = YAML.load_file(file) + chomp_each(data) + dump(file, data) + end + end + + def self.chomp_each(hash) + hash.each do |_key, value| + if value.is_a?(String) + trim(value) + elsif value.is_a?(Array) + strip_array(value) + elsif value + chomp_each(value) + end + end + end + + def self.dump(file, data) + File.open(file, 'w') { |io| io.puts YAML.dump(data) } + end + + def self.strip_array(value) + value.each { |str| trim(str) if str } + end + + def self.trim(str) + str.sub!(/\A\n+/, '') + ended_with_space_after_colon = str =~ /: \s*\Z/ + str.rstrip! + str << ' ' if ended_with_space_after_colon + end +end diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000000..1571492561e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2338 @@ +{ + "name": "upaya", + "version": "0.0.1", + "lockfileVersion": 1, + "dependencies": { + "acorn": { + "version": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", + "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=", + "dev": true + }, + "acorn-jsx": { + "version": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", + "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", + "dev": true, + "dependencies": { + "acorn": { + "version": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", + "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", + "dev": true + } + } + }, + "ajv": { + "version": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", + "dev": true, + "dependencies": { + "json-stable-stringify": { + "version": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "dev": true + } + } + }, + "ajv-keywords": { + "version": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-1.5.1.tgz", + "integrity": "sha1-MU3QpLM2j609/NxU7eYXG4htrzw=", + "dev": true + }, + "amdefine": { + "version": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "dev": true, + "optional": true + }, + "ansi-escapes": { + "version": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", + "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=", + "dev": true + }, + "ansi-regex": { + "version": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "app": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/app/-/app-0.1.0.tgz", + "integrity": "sha1-eT4S9R98zgkiwjFlJhVDeGp0f+o=" + }, + "app-client": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/app-client/-/app-client-0.1.0.tgz", + "integrity": "sha1-K1pOexcCqmX92MnOZoN4o1hgcJ0=" + }, + "argparse": { + "version": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", + "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=", + "dev": true + }, + "array-filter": { + "version": "https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz", + "integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw=", + "dev": true + }, + "array-map": { + "version": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz", + "integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=", + "dev": true + }, + "array-reduce": { + "version": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz", + "integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=", + "dev": true + }, + "array-union": { + "version": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true + }, + "array-uniq": { + "version": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, + "arrify": { + "version": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, + "asn1.js": { + "version": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.9.1.tgz", + "integrity": "sha1-SLokC0WpKA6UdImQull9IWYX/UA=", + "dev": true + }, + "assert": { + "version": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz", + "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=", + "dev": true + }, + "assertion-error": { + "version": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.2.tgz", + "integrity": "sha1-E8pRXYYgbaC6xm6DTdOX2HWBCUw=", + "dev": true + }, + "astw": { + "version": "https://registry.npmjs.org/astw/-/astw-2.2.0.tgz", + "integrity": "sha1-e9QXhNMkk5h66yOba04cV6hzuRc=", + "dev": true + }, + "async": { + "version": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "babel-code-frame": { + "version": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.22.0.tgz", + "integrity": "sha1-AnYgvuVnqIwyVhV05/0IAdMxGOQ=", + "dev": true + }, + "babel-core": { + "version": "https://registry.npmjs.org/babel-core/-/babel-core-6.24.1.tgz", + "integrity": "sha1-jEKFZNzh4fQfszfsNPTDsCK1rYM=", + "dev": true + }, + "babel-eslint": { + "version": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-7.2.3.tgz", + "integrity": "sha1-sv4tgBJkcPXBlELcdXJTqJdxCCc=", + "dev": true + }, + "babel-generator": { + "version": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.24.1.tgz", + "integrity": "sha1-5xX0hsWN7SVknYiJRNUqoHxdlJc=", + "dev": true, + "dependencies": { + "jsesc": { + "version": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", + "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", + "dev": true + } + } + }, + "babel-helper-call-delegate": { + "version": "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz", + "integrity": "sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340=", + "dev": true + }, + "babel-helper-define-map": { + "version": "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.24.1.tgz", + "integrity": "sha1-epdH8ljYlH0y1RX2qhx70CIEoIA=", + "dev": true + }, + "babel-helper-function-name": { + "version": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", + "integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=", + "dev": true + }, + "babel-helper-get-function-arity": { + "version": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", + "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=", + "dev": true + }, + "babel-helper-hoist-variables": { + "version": "https://registry.npmjs.org/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz", + "integrity": "sha1-HssnaJydJVE+rbyZFKc/VAi+enY=", + "dev": true + }, + "babel-helper-optimise-call-expression": { + "version": "https://registry.npmjs.org/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz", + "integrity": "sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc=", + "dev": true + }, + "babel-helper-regex": { + "version": "https://registry.npmjs.org/babel-helper-regex/-/babel-helper-regex-6.24.1.tgz", + "integrity": "sha1-024i+rEAjXnYhkjjIRaGgShFbOg=", + "dev": true + }, + "babel-helper-replace-supers": { + "version": "https://registry.npmjs.org/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz", + "integrity": "sha1-v22/5Dk40XNpohPKiov3S2qQqxo=", + "dev": true + }, + "babel-helpers": { + "version": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", + "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=", + "dev": true + }, + "babel-messages": { + "version": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", + "dev": true + }, + "babel-plugin-check-es2015-constants": { + "version": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz", + "integrity": "sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o=", + "dev": true + }, + "babel-plugin-transform-es2015-arrow-functions": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz", + "integrity": "sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE=", + "dev": true + }, + "babel-plugin-transform-es2015-block-scoped-functions": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz", + "integrity": "sha1-u8UbSflk1wy42OC5ToICRs46YUE=", + "dev": true + }, + "babel-plugin-transform-es2015-block-scoping": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.24.1.tgz", + "integrity": "sha1-dsKV3DpHQbFmWt/TFnIV3P8ypXY=", + "dev": true + }, + "babel-plugin-transform-es2015-classes": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz", + "integrity": "sha1-WkxYpQyclGHlZLSyo7+ryXolhNs=", + "dev": true + }, + "babel-plugin-transform-es2015-computed-properties": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz", + "integrity": "sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM=", + "dev": true + }, + "babel-plugin-transform-es2015-destructuring": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz", + "integrity": "sha1-mXux8auWf2gtKwh2/jWNYOdlxW0=", + "dev": true + }, + "babel-plugin-transform-es2015-duplicate-keys": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz", + "integrity": "sha1-c+s9MQypaePvnskcU3QabxV2Qj4=", + "dev": true + }, + "babel-plugin-transform-es2015-for-of": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz", + "integrity": "sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE=", + "dev": true + }, + "babel-plugin-transform-es2015-function-name": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz", + "integrity": "sha1-g0yJhTvDaxrw86TF26qU/Y6sqos=", + "dev": true + }, + "babel-plugin-transform-es2015-literals": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz", + "integrity": "sha1-T1SgLWzWbPkVKAAZox0xklN3yi4=", + "dev": true + }, + "babel-plugin-transform-es2015-modules-amd": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz", + "integrity": "sha1-Oz5UAXI5hC1tGcMBHEvS8AoA0VQ=", + "dev": true + }, + "babel-plugin-transform-es2015-modules-commonjs": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.24.1.tgz", + "integrity": "sha1-0+MQtA72ZKNmIiAAl8bUQCmPK/4=", + "dev": true + }, + "babel-plugin-transform-es2015-modules-systemjs": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz", + "integrity": "sha1-/4mhQrkRmpBhlfXxBuzzBdlAfSM=", + "dev": true + }, + "babel-plugin-transform-es2015-modules-umd": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz", + "integrity": "sha1-rJl+YoXNGO1hdq22B9YCNErThGg=", + "dev": true + }, + "babel-plugin-transform-es2015-object-super": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz", + "integrity": "sha1-JM72muIcuDp/hgPa0CH1cusnj40=", + "dev": true + }, + "babel-plugin-transform-es2015-parameters": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz", + "integrity": "sha1-V6w1GrScrxSpfNE7CfZv3wpiXys=", + "dev": true + }, + "babel-plugin-transform-es2015-shorthand-properties": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz", + "integrity": "sha1-JPh11nIch2YbvZmkYi5R8U3jiqA=", + "dev": true + }, + "babel-plugin-transform-es2015-spread": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz", + "integrity": "sha1-1taKmfia7cRTbIGlQujdnxdG+NE=", + "dev": true + }, + "babel-plugin-transform-es2015-sticky-regex": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz", + "integrity": "sha1-AMHNsaynERLN8M9hJsLta0V8zbw=", + "dev": true + }, + "babel-plugin-transform-es2015-template-literals": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz", + "integrity": "sha1-qEs0UPfp+PH2g51taH2oS7EjbY0=", + "dev": true + }, + "babel-plugin-transform-es2015-typeof-symbol": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz", + "integrity": "sha1-3sCfHN3/lLUqxz1QXITfWdzOs3I=", + "dev": true + }, + "babel-plugin-transform-es2015-unicode-regex": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz", + "integrity": "sha1-04sS9C6nMj9yk4fxinxa4frrNek=", + "dev": true + }, + "babel-plugin-transform-regenerator": { + "version": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.24.1.tgz", + "integrity": "sha1-uNowWtQ8PJm0hI5P5AN7dw0jxBg=", + "dev": true + }, + "babel-plugin-transform-strict-mode": { + "version": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz", + "integrity": "sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=", + "dev": true + }, + "babel-preset-es2015": { + "version": "https://registry.npmjs.org/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz", + "integrity": "sha1-1EBQ1rwsn+6nAqrzjXJ6AhBTiTk=", + "dev": true + }, + "babel-register": { + "version": "https://registry.npmjs.org/babel-register/-/babel-register-6.24.1.tgz", + "integrity": "sha1-fhDhOi9xBlvfrVoXh7pFvKbe118=", + "dev": true + }, + "babel-runtime": { + "version": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.23.0.tgz", + "integrity": "sha1-CpSJ8UTecO+zzkMArM2zKeL8VDs=", + "dev": true + }, + "babel-template": { + "version": "https://registry.npmjs.org/babel-template/-/babel-template-6.24.1.tgz", + "integrity": "sha1-BK5RTx+Ts6JTfyoPYKWkX7gwgzM=", + "dev": true + }, + "babel-traverse": { + "version": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.24.1.tgz", + "integrity": "sha1-qzZnP9NW+aCUhlnnszjV/q2zFpU=", + "dev": true + }, + "babel-types": { + "version": "https://registry.npmjs.org/babel-types/-/babel-types-6.24.1.tgz", + "integrity": "sha1-oTaHncFbNga9oNkMH8dDBML/CXU=", + "dev": true + }, + "babelify": { + "version": "https://registry.npmjs.org/babelify/-/babelify-7.3.0.tgz", + "integrity": "sha1-qlau3nBn/XvVSWZu4W3ChQh+iOU=", + "dev": true + }, + "babylon": { + "version": "https://registry.npmjs.org/babylon/-/babylon-6.17.2.tgz", + "integrity": "sha1-IB0l71+JLEG65JSIsI2w3Udun1w=", + "dev": true + }, + "balanced-match": { + "version": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=", + "dev": true + }, + "base64-js": { + "version": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.0.tgz", + "integrity": "sha1-o5mS1yNYSBGYK+XikLtqU9hnAPE=", + "dev": true + }, + "basscss": { + "version": "https://registry.npmjs.org/basscss/-/basscss-7.1.1.tgz", + "integrity": "sha1-e/MSAxl6Kd8O7lUfQlYXqL58d3s=" + }, + "basscss-align": { + "version": "https://registry.npmjs.org/basscss-align/-/basscss-align-1.0.2.tgz", + "integrity": "sha1-KUqmidb5nahuSvTFwokocIVcHDc=" + }, + "basscss-background-colors": { + "version": "https://registry.npmjs.org/basscss-background-colors/-/basscss-background-colors-1.1.3.tgz", + "integrity": "sha1-VKKDZRxAklZTJKTvW8JdcL52IdY=" + }, + "basscss-base-forms": { + "version": "https://registry.npmjs.org/basscss-base-forms/-/basscss-base-forms-2.0.2.tgz", + "integrity": "sha1-Fgi5n/SG3WuJEZLKjSXi7VdYWao=" + }, + "basscss-base-reset": { + "version": "https://registry.npmjs.org/basscss-base-reset/-/basscss-base-reset-2.0.3.tgz", + "integrity": "sha1-WScWF55JfgtOfJrdHxxMNAF8Vy0=" + }, + "basscss-base-tables": { + "version": "https://registry.npmjs.org/basscss-base-tables/-/basscss-base-tables-1.0.2.tgz", + "integrity": "sha1-uFDqHWSwb5GSK/z0AUuqoINquzA=" + }, + "basscss-base-typography": { + "version": "https://registry.npmjs.org/basscss-base-typography/-/basscss-base-typography-2.0.3.tgz", + "integrity": "sha1-H0vzRXEkgoII9oa8OFzTMK5+enI=" + }, + "basscss-border": { + "version": "https://registry.npmjs.org/basscss-border/-/basscss-border-3.0.4.tgz", + "integrity": "sha1-ZZk1aNoIZ+t12Wtwn5fjOyuQJIY=" + }, + "basscss-border-colors": { + "version": "https://registry.npmjs.org/basscss-border-colors/-/basscss-border-colors-1.1.3.tgz", + "integrity": "sha1-nrIya0eeqpe/m9bE7rwf2CTYCf4=" + }, + "basscss-borders": { + "version": "https://registry.npmjs.org/basscss-borders/-/basscss-borders-2.0.5.tgz", + "integrity": "sha1-PYRw+6kOzoknBeGlskwna3Oe644=" + }, + "basscss-btn": { + "version": "https://registry.npmjs.org/basscss-btn/-/basscss-btn-1.1.1.tgz", + "integrity": "sha1-xCFX8gG9lduaJRVoxULnQLKj178=" + }, + "basscss-btn-outline": { + "version": "https://registry.npmjs.org/basscss-btn-outline/-/basscss-btn-outline-1.1.0.tgz", + "integrity": "sha1-uEROqdPVCM0Adgqdb9/0uvXUJ1g=" + }, + "basscss-btn-primary": { + "version": "https://registry.npmjs.org/basscss-btn-primary/-/basscss-btn-primary-1.1.0.tgz", + "integrity": "sha1-DBJJKXHiFuQr3xNEEa/DgCw9i/c=" + }, + "basscss-color-base": { + "version": "https://registry.npmjs.org/basscss-color-base/-/basscss-color-base-2.0.2.tgz", + "integrity": "sha1-7YSL/OORq1NabRSnAqFPpMTzJC0=" + }, + "basscss-color-forms": { + "version": "https://registry.npmjs.org/basscss-color-forms/-/basscss-color-forms-3.0.2.tgz", + "integrity": "sha1-jy0dB9X8tmRVbNNUvvmM67LV4Ms=" + }, + "basscss-color-tables": { + "version": "https://registry.npmjs.org/basscss-color-tables/-/basscss-color-tables-1.0.4.tgz", + "integrity": "sha1-1DXsfF8hD6F959G4gT3rvKalQ+E=" + }, + "basscss-colors": { + "version": "https://registry.npmjs.org/basscss-colors/-/basscss-colors-2.2.0.tgz", + "integrity": "sha1-3Mt3Picu/kXfSkgJYsi9iLams+E=", + "dependencies": { + "colors.css": { + "version": "https://registry.npmjs.org/colors.css/-/colors.css-3.0.0.tgz", + "integrity": "sha1-URz0L7inGZqMvvSciKTqTx2Pnvw=" + } + } + }, + "basscss-defaults": { + "version": "https://registry.npmjs.org/basscss-defaults/-/basscss-defaults-2.1.3.tgz", + "integrity": "sha1-tOpjToFcaSPwx2ZbFIpMyLO0OSc=" + }, + "basscss-grid": { + "version": "https://registry.npmjs.org/basscss-grid/-/basscss-grid-1.0.6.tgz", + "integrity": "sha1-GlEsc7h0MwXkejanQyqtXCbMKGc=" + }, + "basscss-layout": { + "version": "https://registry.npmjs.org/basscss-layout/-/basscss-layout-3.1.0.tgz", + "integrity": "sha1-+fOS5IDaZmV9n+XenKTAfFecOk4=" + }, + "basscss-margin": { + "version": "https://registry.npmjs.org/basscss-margin/-/basscss-margin-1.0.7.tgz", + "integrity": "sha1-WpLYzamO85HHOhXt6Xs0tIiGQXw=" + }, + "basscss-padding": { + "version": "https://registry.npmjs.org/basscss-padding/-/basscss-padding-1.1.3.tgz", + "integrity": "sha1-adt5lBTm3Vi+2Dd2lSzCmeLmh04=" + }, + "basscss-position": { + "version": "https://registry.npmjs.org/basscss-position/-/basscss-position-2.0.3.tgz", + "integrity": "sha1-RnGAofjzhukHLtjQgpTSpuC6QwU=" + }, + "basscss-positions": { + "version": "https://registry.npmjs.org/basscss-positions/-/basscss-positions-1.0.5.tgz", + "integrity": "sha1-5P37bQMc8ljGERf5M3Hzq7uTiBo=" + }, + "basscss-responsive-states": { + "version": "https://registry.npmjs.org/basscss-responsive-states/-/basscss-responsive-states-1.0.6.tgz", + "integrity": "sha1-2JI0PheZiFwD5PHHAs18GrUo8AI=" + }, + "basscss-sass": { + "version": "https://registry.npmjs.org/basscss-sass/-/basscss-sass-3.0.0.tgz", + "integrity": "sha1-nxvoX6jqafmUQVN2ImjEavMLxM0=" + }, + "basscss-type-scale": { + "version": "https://registry.npmjs.org/basscss-type-scale/-/basscss-type-scale-1.0.5.tgz", + "integrity": "sha1-I79eQcnRQsgGHPmCnM8j6bMljsc=" + }, + "basscss-typography": { + "version": "https://registry.npmjs.org/basscss-typography/-/basscss-typography-3.0.3.tgz", + "integrity": "sha1-GCz0PffE6+0CdQ3HSAQcut/2DUM=" + }, + "bluebird": { + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.10.2.tgz", + "integrity": "sha1-AkpVFylTCIV/FPkfEQb8O1VfRGs=" + }, + "bn.js": { + "version": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.6.tgz", + "integrity": "sha1-UzRK2xRhehP26N0s4okF0cC6MhU=", + "dev": true + }, + "brace-expansion": { + "version": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.7.tgz", + "integrity": "sha1-Pv/DxQ4ABTH7cg6v+A8K6O8jz1k=", + "dev": true + }, + "brorand": { + "version": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", + "dev": true + }, + "browser-pack": { + "version": "https://registry.npmjs.org/browser-pack/-/browser-pack-6.0.2.tgz", + "integrity": "sha1-+GzWzvT1MAyOY+B6TVEvZfv/RTE=", + "dev": true + }, + "browser-resolve": { + "version": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.2.tgz", + "integrity": "sha1-j/CbCixCFxihBRwmCzLkj0QpOM4=", + "dev": true, + "dependencies": { + "resolve": { + "version": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + } + } + }, + "browserify": { + "version": "https://registry.npmjs.org/browserify/-/browserify-13.3.0.tgz", + "integrity": "sha1-tanJAgJD8McORnW+yCI7xifkFc4=", + "dev": true + }, + "browserify-aes": { + "version": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.0.6.tgz", + "integrity": "sha1-Xncl297x/Vkw1OurSFZ85FHEigo=", + "dev": true + }, + "browserify-cache-api": { + "version": "https://registry.npmjs.org/browserify-cache-api/-/browserify-cache-api-3.0.1.tgz", + "integrity": "sha1-liR+hT8Gj9bg1FzHPwuyzZd47wI=", + "dev": true + }, + "browserify-cipher": { + "version": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.0.tgz", + "integrity": "sha1-mYgkSHS/XtTijalWZtzWasj8Njo=", + "dev": true + }, + "browserify-des": { + "version": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.0.tgz", + "integrity": "sha1-2qJ3cXRwki7S/hhZQRihdUOXId0=", + "dev": true + }, + "browserify-incremental": { + "version": "https://registry.npmjs.org/browserify-incremental/-/browserify-incremental-3.1.1.tgz", + "integrity": "sha1-BxPLdYckemMqnwjPG9FpuHi2Koo=", + "dev": true, + "dependencies": { + "jsonparse": { + "version": "https://registry.npmjs.org/jsonparse/-/jsonparse-0.0.5.tgz", + "integrity": "sha1-MwVCrT8KZUZlt3jz6y2an6UHrGQ=", + "dev": true + }, + "JSONStream": { + "version": "https://registry.npmjs.org/JSONStream/-/JSONStream-0.10.0.tgz", + "integrity": "sha1-dDSdDYlSK3HzDwoD/5vSDKbxKsA=", + "dev": true + } + } + }, + "browserify-rsa": { + "version": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "dev": true + }, + "browserify-sign": { + "version": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", + "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", + "dev": true + }, + "browserify-zlib": { + "version": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz", + "integrity": "sha1-uzX4pRn2AOD6a4SFJByXnQFB+y0=", + "dev": true + }, + "bson": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-1.0.4.tgz", + "integrity": "sha1-k8ENOeqltYQVy8QFLz5T5WKwtyw=" + }, + "buffer": { + "version": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "dev": true + }, + "buffer-shims": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", + "integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E=" + }, + "buffer-xor": { + "version": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", + "dev": true + }, + "builtin-modules": { + "version": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "builtin-status-codes": { + "version": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", + "dev": true + }, + "cached-path-relative": { + "version": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.0.1.tgz", + "integrity": "sha1-0JxLUoAKpMB44t2BqGmqyQ0uVOc=", + "dev": true + }, + "caller-path": { + "version": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", + "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", + "dev": true + }, + "callsites": { + "version": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", + "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", + "dev": true + }, + "chai": { + "version": "https://registry.npmjs.org/chai/-/chai-3.5.0.tgz", + "integrity": "sha1-TQJjewZ/6Vi9v906QOxW/vc3Mkc=", + "dev": true + }, + "chalk": { + "version": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true + }, + "cipher-base": { + "version": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.3.tgz", + "integrity": "sha1-7qvxlEGc6QDaMBjCB9IS8qbfCgc=", + "dev": true + }, + "circular-json": { + "version": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.1.tgz", + "integrity": "sha1-vos2rvzN6LPKeqLWr8B6NyQsDS0=", + "dev": true + }, + "classlist.js": { + "version": "https://registry.npmjs.org/classlist.js/-/classlist.js-1.1.20150312.tgz", + "integrity": "sha1-HXCEL3Ai8I2awIbOaeWyUPLFd4k=" + }, + "cli-cursor": { + "version": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", + "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", + "dev": true + }, + "cli-width": { + "version": "https://registry.npmjs.org/cli-width/-/cli-width-2.1.0.tgz", + "integrity": "sha1-sjTKIJsp72b8UY2bmNWEewDt8Ao=", + "dev": true + }, + "clipboard": { + "version": "https://registry.npmjs.org/clipboard/-/clipboard-1.7.1.tgz", + "integrity": "sha1-Ng1taUbpmnof7zleQrqStem1oWs=" + }, + "co": { + "version": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, + "code-point-at": { + "version": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "colors.css": { + "version": "https://registry.npmjs.org/colors.css/-/colors.css-2.3.0.tgz", + "integrity": "sha1-6JU4N1Q+GdmOKRf/C5mPbbKGITs=" + }, + "combine-source-map": { + "version": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.7.2.tgz", + "integrity": "sha1-CHAxKFazB6h8xKxIbzqaYq7MwJ4=", + "dev": true, + "dependencies": { + "convert-source-map": { + "version": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz", + "integrity": "sha1-SCnId+n+SbMWHzvzZziI4gRpmGA=", + "dev": true + } + } + }, + "commander": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", + "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=" + }, + "concat-map": { + "version": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "concat-stream": { + "version": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.2.tgz", + "integrity": "sha1-cIl4Yk2FavQaWnQd790mHadSwmY=", + "dev": true, + "dependencies": { + "readable-stream": { + "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", + "dev": true + } + } + }, + "connect": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.6.2.tgz", + "integrity": "sha1-aU6NIGgb/kkCgsiriGvpjwn0L+c=", + "dependencies": { + "debug": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.7.tgz", + "integrity": "sha1-krrR9tBbu2u6Isyoi80OyJTChh4=" + } + } + }, + "console-browserify": { + "version": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", + "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", + "dev": true + }, + "constants-browserify": { + "version": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", + "dev": true + }, + "contains-path": { + "version": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", + "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", + "dev": true + }, + "convert-source-map": { + "version": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.0.tgz", + "integrity": "sha1-ms1whRxtXf3ZPZKC5e35SgP/RrU=", + "dev": true + }, + "core-js": { + "version": "https://registry.npmjs.org/core-js/-/core-js-2.4.1.tgz", + "integrity": "sha1-TekR5mew6ukSTjQlS1OupvxhjT4=", + "dev": true + }, + "core-util-is": { + "version": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cornerstone": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/cornerstone/-/cornerstone-0.1.1.tgz", + "integrity": "sha1-D4mxPUZVrgAEgUZpOWwZUqnXjt0=" + }, + "create-ecdh": { + "version": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.0.tgz", + "integrity": "sha1-iIxyNZbN92EvZJgjPuvXo1MBc30=", + "dev": true + }, + "create-hash": { + "version": "https://registry.npmjs.org/create-hash/-/create-hash-1.1.3.tgz", + "integrity": "sha1-YGBCrIuSYnUPSDyt2rD1gZFy2P0=", + "dev": true + }, + "create-hmac": { + "version": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.6.tgz", + "integrity": "sha1-rLniIaThe9sHbpBlfEK5PjcmzwY=", + "dev": true + }, + "crypto-browserify": { + "version": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.11.0.tgz", + "integrity": "sha1-NlKgkGq5sqfgw85mpAjpV6JIVSI=", + "dev": true + }, + "d": { + "version": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", + "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", + "dev": true + }, + "date-now": { + "version": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", + "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", + "dev": true + }, + "debug": { + "version": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", + "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=" + }, + "deep-eql": { + "version": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", + "integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=", + "dev": true, + "dependencies": { + "type-detect": { + "version": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz", + "integrity": "sha1-C6XsKohWQORw6k6FBZcZANrFiCI=", + "dev": true + } + } + }, + "deep-is": { + "version": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "defined": { + "version": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", + "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=", + "dev": true + }, + "del": { + "version": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", + "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", + "dev": true + }, + "delegate": { + "version": "https://registry.npmjs.org/delegate/-/delegate-3.1.3.tgz", + "integrity": "sha1-moJRp3fXAl+qVXN7w7BxdCEnqf0=" + }, + "deps-sort": { + "version": "https://registry.npmjs.org/deps-sort/-/deps-sort-2.0.0.tgz", + "integrity": "sha1-CRckkC6EZYJg65EHSMzNGvbiH7U=", + "dev": true + }, + "des.js": { + "version": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", + "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", + "dev": true + }, + "detect-indent": { + "version": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", + "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", + "dev": true + }, + "detective": { + "version": "https://registry.npmjs.org/detective/-/detective-4.5.0.tgz", + "integrity": "sha1-blqMaybmx6JUsca210kNmOyR7dE=", + "dev": true + }, + "diffie-hellman": { + "version": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.2.tgz", + "integrity": "sha1-tYNXOScM/ias9jIJn97SoH8gnl4=", + "dev": true + }, + "dirty-chai": { + "version": "https://registry.npmjs.org/dirty-chai/-/dirty-chai-1.2.2.tgz", + "integrity": "sha1-eEleYZY19/5EIZqkyDeEm/GDFC4=", + "dev": true + }, + "doctrine": { + "version": "https://registry.npmjs.org/doctrine/-/doctrine-2.0.0.tgz", + "integrity": "sha1-xz2NKQnSIpHhoAejlYBNqLZl/mM=", + "dev": true + }, + "domain-browser": { + "version": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.1.7.tgz", + "integrity": "sha1-hnqksJP6oF8d4IwG9NeyH9+GmLw=", + "dev": true + }, + "duplexer2": { + "version": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", + "dev": true + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "elliptic": { + "version": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz", + "integrity": "sha1-ysmvh2LIWDYYcAPI3+GT5eLq5d8=", + "dev": true + }, + "encodeurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz", + "integrity": "sha1-eePVhlU0aQn+bw9Fpd5oEDspTSA=" + }, + "error-ex": { + "version": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", + "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", + "dev": true + }, + "es5-ext": { + "version": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.22.tgz", + "integrity": "sha1-GHbFH5kHacESx4HqPr6J+E/TkHE=", + "dev": true + }, + "es6-iterator": { + "version": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.1.tgz", + "integrity": "sha1-jjGcnwRTv1ddN0lAplWSDlnKVRI=", + "dev": true + }, + "es6-map": { + "version": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", + "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=", + "dev": true + }, + "es6-promise": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.2.1.tgz", + "integrity": "sha1-7FYjOGgDKQkgcXDDlEjiREndH8Q=" + }, + "es6-set": { + "version": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", + "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=", + "dev": true + }, + "es6-symbol": { + "version": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", + "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", + "dev": true + }, + "es6-weak-map": { + "version": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz", + "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=", + "dev": true + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "escape-string-regexp": { + "version": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "escodegen": { + "version": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", + "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", + "dev": true, + "dependencies": { + "esprima": { + "version": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", + "dev": true + }, + "estraverse": { + "version": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", + "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=", + "dev": true + }, + "source-map": { + "version": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", + "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", + "dev": true, + "optional": true + } + } + }, + "escope": { + "version": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz", + "integrity": "sha1-4Bl16BJ4GhY6ba392AOY3GTIicM=", + "dev": true + }, + "eslint": { + "version": "https://registry.npmjs.org/eslint/-/eslint-3.19.0.tgz", + "integrity": "sha1-yPxiAcf0DdCJQbh8CFdnOGpnmsw=", + "dev": true, + "dependencies": { + "json-stable-stringify": { + "version": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "dev": true + } + } + }, + "eslint-config-airbnb-base": { + "version": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-11.2.0.tgz", + "integrity": "sha1-GancRIGib3CQRUXsBAEWh2AY+FM=", + "dev": true + }, + "eslint-import-resolver-node": { + "version": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.2.3.tgz", + "integrity": "sha1-Wt2BBujJKNssuiMrzZ76hG49oWw=", + "dev": true + }, + "eslint-module-utils": { + "version": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.0.0.tgz", + "integrity": "sha1-pvjCHZATWHWc3DXbrBmCrh7li84=", + "dev": true, + "dependencies": { + "debug": { + "version": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "dev": true + }, + "ms": { + "version": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", + "dev": true + } + } + }, + "eslint-plugin-import": { + "version": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.3.0.tgz", + "integrity": "sha1-N8gB4K2g4pbL3yDD85OstbUq82s=", + "dev": true, + "dependencies": { + "doctrine": { + "version": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", + "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", + "dev": true + } + } + }, + "espree": { + "version": "https://registry.npmjs.org/espree/-/espree-3.4.3.tgz", + "integrity": "sha1-KRC1zNSc6JPC//+qtP2LOjG4I3Q=", + "dev": true, + "dependencies": { + "acorn": { + "version": "https://registry.npmjs.org/acorn/-/acorn-5.0.3.tgz", + "integrity": "sha1-xGDfCEkUY/AozLguqzcwvwEIez0=", + "dev": true + } + } + }, + "esprima": { + "version": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", + "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", + "dev": true + }, + "esquery": { + "version": "https://registry.npmjs.org/esquery/-/esquery-1.0.0.tgz", + "integrity": "sha1-z7qLV9f7qT8XKYqKAGoEzaE9gPo=", + "dev": true + }, + "esrecurse": { + "version": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.1.0.tgz", + "integrity": "sha1-RxO2U2rffyrE8yfVWed1a/9kgiA=", + "dev": true, + "dependencies": { + "estraverse": { + "version": "https://registry.npmjs.org/estraverse/-/estraverse-4.1.1.tgz", + "integrity": "sha1-9srKcokzqFDvkGYdDheYK6RxEaI=", + "dev": true + } + } + }, + "estraverse": { + "version": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + }, + "esutils": { + "version": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "event-emitter": { + "version": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", + "dev": true + }, + "events": { + "version": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", + "dev": true + }, + "evp_bytestokey": { + "version": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.0.tgz", + "integrity": "sha1-SXtmrZ/vZc18CKYYCCS6FHa2blM=", + "dev": true + }, + "exit-hook": { + "version": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", + "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=", + "dev": true + }, + "fast-levenshtein": { + "version": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "field-kit": { + "version": "https://registry.npmjs.org/field-kit/-/field-kit-2.1.0.tgz", + "integrity": "sha1-5o7eX04wUbLcQlgQWklcVBo7LX8=" + }, + "figures": { + "version": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "dev": true + }, + "file-entry-cache": { + "version": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", + "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", + "dev": true + }, + "fill-keys": { + "version": "https://registry.npmjs.org/fill-keys/-/fill-keys-1.0.2.tgz", + "integrity": "sha1-mo+jb06K1jTjv2tPPIiCVRRS6yA=", + "dev": true + }, + "finalhandler": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.0.3.tgz", + "integrity": "sha1-70fneVDpmXgOhgIqVg4yF+DQzIk=", + "dependencies": { + "debug": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.7.tgz", + "integrity": "sha1-krrR9tBbu2u6Isyoi80OyJTChh4=" + } + } + }, + "find-up": { + "version": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true + }, + "flat-cache": { + "version": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.2.2.tgz", + "integrity": "sha1-+oZxTnLCHbiGAXYezy9VXRq8a5Y=", + "dev": true + }, + "flex-object": { + "version": "https://registry.npmjs.org/flex-object/-/flex-object-2.0.5.tgz", + "integrity": "sha1-Ebm7wPT4ZOncYH6YzixnBEK9yFE=" + }, + "focus-trap": { + "version": "https://registry.npmjs.org/focus-trap/-/focus-trap-2.3.0.tgz", + "integrity": "sha1-B8kZZIZ9NGMV9PX434i/lkVTFuI=" + }, + "formatio": { + "version": "https://registry.npmjs.org/formatio/-/formatio-1.1.1.tgz", + "integrity": "sha1-XtPM1jZVEJc4NGXZlhmRAOhhYek=" + }, + "fs.realpath": { + "version": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "function-bind": { + "version": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.0.tgz", + "integrity": "sha1-FhdnFMgBeY5Ojyz391KUZ7tKV3E=", + "dev": true + }, + "generate-function": { + "version": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", + "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=", + "dev": true + }, + "generate-object-property": { + "version": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", + "dev": true + }, + "glob": { + "version": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=", + "dev": true + }, + "globals": { + "version": "https://registry.npmjs.org/globals/-/globals-9.17.0.tgz", + "integrity": "sha1-DAymltm5u2lNLlRwvTd3fKrVAoY=", + "dev": true + }, + "globby": { + "version": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", + "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", + "dev": true + }, + "good-listener": { + "version": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", + "integrity": "sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=" + }, + "graceful-fs": { + "version": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true + }, + "graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=" + }, + "has": { + "version": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", + "integrity": "sha1-hGFzP1OLCDfJNh45qauelwTcLyg=", + "dev": true + }, + "has-ansi": { + "version": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true + }, + "has-require": { + "version": "https://registry.npmjs.org/has-require/-/has-require-1.2.2.tgz", + "integrity": "sha1-khZ1qxMNvZdo/I2o8ajiQt+kF3Q=", + "dev": true + }, + "hash-base": { + "version": "https://registry.npmjs.org/hash-base/-/hash-base-2.0.2.tgz", + "integrity": "sha1-ZuodhW206KVHDK32/OI65SRO8uE=", + "dev": true + }, + "hash.js": { + "version": "https://registry.npmjs.org/hash.js/-/hash.js-1.0.3.tgz", + "integrity": "sha1-EzL/ABVsCg/92CNgE9B7d6BFFXM=", + "dev": true + }, + "hint.css": { + "version": "https://registry.npmjs.org/hint.css/-/hint.css-2.5.0.tgz", + "integrity": "sha1-OMrjZn5C2R392+UDEAqzSTL2/WU=" + }, + "hmac-drbg": { + "version": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "dev": true + }, + "home-or-tmp": { + "version": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", + "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=", + "dev": true + }, + "hooks-fixed": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hooks-fixed/-/hooks-fixed-2.0.0.tgz", + "integrity": "sha1-oB2JTVKsf2WZu7H2PfycQR33DLo=" + }, + "hosted-git-info": { + "version": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.4.2.tgz", + "integrity": "sha1-AHa59GonBQbduq6lZJaJdGBhKmc=", + "dev": true + }, + "htmlescape": { + "version": "https://registry.npmjs.org/htmlescape/-/htmlescape-1.1.1.tgz", + "integrity": "sha1-OgPtwiFLyjtmQko+eVk0lQnLA1E=", + "dev": true + }, + "https-browserify": { + "version": "https://registry.npmjs.org/https-browserify/-/https-browserify-0.0.1.tgz", + "integrity": "sha1-P5E2XKvmC3ftDruiS0VOPgnZWoI=", + "dev": true + }, + "ieee754": { + "version": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", + "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=", + "dev": true + }, + "ignore": { + "version": "https://registry.npmjs.org/ignore/-/ignore-3.3.3.tgz", + "integrity": "sha1-QyNS5XrM2HqzEQ6C0/6g5HgSFW0=", + "dev": true + }, + "imurmurhash": { + "version": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "indexof": { + "version": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=", + "dev": true + }, + "inflight": { + "version": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true + }, + "inherits": { + "version": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=" + }, + "inline-source-map": { + "version": "https://registry.npmjs.org/inline-source-map/-/inline-source-map-0.6.2.tgz", + "integrity": "sha1-+Tk0ccGKedFyT4Y/o4tYY3Ct4qU=", + "dev": true + }, + "input-sim": { + "version": "https://registry.npmjs.org/input-sim/-/input-sim-3.1.0.tgz", + "integrity": "sha1-g/nCFPTW2MjpCU00V5eVkeqIzCw=" + }, + "inquirer": { + "version": "https://registry.npmjs.org/inquirer/-/inquirer-0.12.0.tgz", + "integrity": "sha1-HvK/1jUE3wvHV4X/+MLEHfEvB34=", + "dev": true + }, + "insert-module-globals": { + "version": "https://registry.npmjs.org/insert-module-globals/-/insert-module-globals-7.0.1.tgz", + "integrity": "sha1-wDv04BywhtW15azorQr+eInWOMM=", + "dev": true + }, + "interpret": { + "version": "https://registry.npmjs.org/interpret/-/interpret-1.0.3.tgz", + "integrity": "sha1-y8NcYu7uc/Gat7EKgBURQBr8D5A=", + "dev": true + }, + "invariant": { + "version": "https://registry.npmjs.org/invariant/-/invariant-2.2.2.tgz", + "integrity": "sha1-nh9WrArNtr8wMwbzOL47IErmA2A=", + "dev": true + }, + "is-arrayish": { + "version": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-buffer": { + "version": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.5.tgz", + "integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw=", + "dev": true + }, + "is-builtin-module": { + "version": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "dev": true + }, + "is-finite": { + "version": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true + }, + "is-my-json-valid": { + "version": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.16.0.tgz", + "integrity": "sha1-8Hndm/2uZe4gOKrorLyGqxCeNpM=", + "dev": true + }, + "is-object": { + "version": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz", + "integrity": "sha1-iVJojF7C/9awPsyF52ngKQMINHA=", + "dev": true + }, + "is-path-cwd": { + "version": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", + "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", + "dev": true + }, + "is-path-in-cwd": { + "version": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz", + "integrity": "sha1-ZHdYK4IU1gI0YJRWcAO+ip6sBNw=", + "dev": true + }, + "is-path-inside": { + "version": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.0.tgz", + "integrity": "sha1-/AbloWg/vaE95mev9xe7wQpI838=", + "dev": true + }, + "is-property": { + "version": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", + "dev": true + }, + "is-resolvable": { + "version": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.0.0.tgz", + "integrity": "sha1-jfV8YeouPFAUCNEA+wE8+NbgzGI=", + "dev": true + }, + "isarray": { + "version": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "js-tokens": { + "version": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.1.tgz", + "integrity": "sha1-COnxMkhKLEWjCQfp3E1VZ7fxFNc=", + "dev": true + }, + "js-yaml": { + "version": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.8.4.tgz", + "integrity": "sha1-UgtFZPhlc7qWZir4Woyvp7S1pvY=", + "dev": true + }, + "jsesc": { + "version": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", + "dev": true + }, + "json-stable-stringify": { + "version": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-0.0.1.tgz", + "integrity": "sha1-YRwj6BTbN1Un34URk9tZ3Sryf0U=", + "dev": true + }, + "json5": { + "version": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", + "dev": true + }, + "jsonify": { + "version": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", + "dev": true + }, + "jsonparse": { + "version": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", + "dev": true + }, + "jsonpointer": { + "version": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", + "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", + "dev": true + }, + "JSONStream": { + "version": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.1.tgz", + "integrity": "sha1-cH92HgHa6eFvG8+TcDt4xwlmV5o=", + "dev": true + }, + "kareem": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-1.4.2.tgz", + "integrity": "sha1-O0r12/rzrBwIuOVRj92BupDCq3I=" + }, + "labeled-stream-splicer": { + "version": "https://registry.npmjs.org/labeled-stream-splicer/-/labeled-stream-splicer-2.0.0.tgz", + "integrity": "sha1-pS4dE4AkwAuGscDJH2d5GLiuClk=", + "dev": true, + "dependencies": { + "isarray": { + "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + } + } + }, + "levn": { + "version": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true + }, + "lexical-scope": { + "version": "https://registry.npmjs.org/lexical-scope/-/lexical-scope-1.2.0.tgz", + "integrity": "sha1-/Ope3HBKSzqHls3KQZw6CvryLfQ=", + "dev": true + }, + "load-json-file": { + "version": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "dev": true + }, + "locate-path": { + "version": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "dependencies": { + "path-exists": { + "version": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + } + } + }, + "lodash": { + "version": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" + }, + "lodash.cond": { + "version": "https://registry.npmjs.org/lodash.cond/-/lodash.cond-4.5.2.tgz", + "integrity": "sha1-9HGh2khr5g9quVXRcRVSPdHSVdU=", + "dev": true + }, + "lodash.memoize": { + "version": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-3.0.4.tgz", + "integrity": "sha1-LcvSwofLwKVcxCMovQxzYVDVPj8=", + "dev": true + }, + "lolex": { + "version": "https://registry.npmjs.org/lolex/-/lolex-1.3.2.tgz", + "integrity": "sha1-fD2mL/yzDw9agKJWbKJORdigHzE=" + }, + "loose-envify": { + "version": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", + "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", + "dev": true + }, + "merge-descriptors": { + "version": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", + "dev": true + }, + "miller-rabin": { + "version": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.0.tgz", + "integrity": "sha1-SmL7HUKTPAVYOYL0xxb2+55sbT0=", + "dev": true + }, + "mime": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.6.tgz", + "integrity": "sha1-WR2E02U6awtKO5343lqoEI5y5eA=" + }, + "minimalistic-assert": { + "version": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz", + "integrity": "sha1-cCvi3aazf0g2vLP121ZkG2Sh09M=", + "dev": true + }, + "minimalistic-crypto-utils": { + "version": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", + "dev": true + }, + "minimatch": { + "version": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", + "dev": true + }, + "minimist": { + "version": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + }, + "mkdirp": { + "version": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true + }, + "modulator": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/modulator/-/modulator-0.1.0.tgz", + "integrity": "sha1-z9UVhD+R1nPxVWHZeZ33gErQMmY=" + }, + "module-deps": { + "version": "https://registry.npmjs.org/module-deps/-/module-deps-4.1.1.tgz", + "integrity": "sha1-IyFYM/HaE/1gbMuAh7RIUty4If0=", + "dev": true + }, + "module-not-found-error": { + "version": "https://registry.npmjs.org/module-not-found-error/-/module-not-found-error-1.0.1.tgz", + "integrity": "sha1-z4tP9PKWQGdNbN0CsOO8UjwrvcA=", + "dev": true + }, + "mongodb": { + "version": "2.2.27", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.2.27.tgz", + "integrity": "sha1-NBIgNNtm2YO89qta2yaiSnD+9uY=", + "dependencies": { + "readable-stream": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.7.tgz", + "integrity": "sha1-BwV6y+JGeyIELTb5jFrVBwVOlbE=" + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==" + } + } + }, + "mongodb-core": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-2.1.11.tgz", + "integrity": "sha1-HDh3bOsXSZepnCiGDu2QKNqbPho=" + }, + "mongoose": { + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-4.11.3.tgz", + "integrity": "sha1-+T1CeygsLnmLD+FTL7Qafd5umNM=", + "dependencies": { + "async": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.1.4.tgz", + "integrity": "sha1-LSFgx3iAMuTdbL4lAvH5osj2zeQ=" + } + } + }, + "mpath": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.3.0.tgz", + "integrity": "sha1-elj3iem1/TyUUgY0FXlg8mvV70Q=" + }, + "mpromise": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mpromise/-/mpromise-0.5.5.tgz", + "integrity": "sha1-9bJCWddjrMIlewoMjG2Gb9UXMuY=" + }, + "mquery": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-2.3.1.tgz", + "integrity": "sha1-mrNnSXFIAP8LtTpoHOS8TV8HyHs=", + "dependencies": { + "sliced": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/sliced/-/sliced-0.0.5.tgz", + "integrity": "sha1-XtwETKTrb3gW1Qui/GPiXY/kcH8=" + } + } + }, + "ms": { + "version": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "muri": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/muri/-/muri-1.2.2.tgz", + "integrity": "sha1-YxmBMmUNsIoEzHnM0A3Tia/SYxw=" + }, + "mute-stream": { + "version": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz", + "integrity": "sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA=", + "dev": true + }, + "natural-compare": { + "version": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "normalize-package-data": { + "version": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.3.8.tgz", + "integrity": "sha1-2Bntoqne29H/pWPqQHHZNngilbs=", + "dev": true + }, + "normalize.css": { + "version": "https://registry.npmjs.org/normalize.css/-/normalize.css-4.2.0.tgz", + "integrity": "sha1-IdZsxVcVTUN5/R4HnsfeWKN5sJk=" + }, + "number-is-nan": { + "version": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "object-assign": { + "version": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=" + }, + "once": { + "version": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true + }, + "onetime": { + "version": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", + "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", + "dev": true + }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "dependencies": { + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" + } + } + }, + "optionator": { + "version": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "dev": true + }, + "os-browserify": { + "version": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.1.2.tgz", + "integrity": "sha1-ScoCk+CxlZCl9d4Qx/JlphfY/lQ=", + "dev": true + }, + "os-homedir": { + "version": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "os-tmpdir": { + "version": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "p-limit": { + "version": "https://registry.npmjs.org/p-limit/-/p-limit-1.1.0.tgz", + "integrity": "sha1-sH/y2aXYi+yAYDWJWiurZqJ5iLw=", + "dev": true + }, + "p-locate": { + "version": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true + }, + "pako": { + "version": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=", + "dev": true + }, + "parents": { + "version": "https://registry.npmjs.org/parents/-/parents-1.0.1.tgz", + "integrity": "sha1-/t1NK/GTp3dF/nHjcdc8MwfZx1E=", + "dev": true + }, + "parse-asn1": { + "version": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.0.tgz", + "integrity": "sha1-N8T5t+06tlx0gXtfJICTf7+XxxI=", + "dev": true + }, + "parse-json": { + "version": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true + }, + "parseurl": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz", + "integrity": "sha1-yKuMkiO6NIiKpkopeyiFO+wY2lY=" + }, + "path-browserify": { + "version": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz", + "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=", + "dev": true + }, + "path-exists": { + "version": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true + }, + "path-is-absolute": { + "version": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-is-inside": { + "version": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "path-parse": { + "version": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", + "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", + "dev": true + }, + "path-platform": { + "version": "https://registry.npmjs.org/path-platform/-/path-platform-0.11.15.tgz", + "integrity": "sha1-6GQhf3TDaFDwhSt43Hv31KVyG/I=", + "dev": true + }, + "path-type": { + "version": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "dev": true + }, + "pbkdf2": { + "version": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.12.tgz", + "integrity": "sha1-vjZ4XFBn6kjYBv+SMojF91C2uKI=", + "dev": true + }, + "pff": { + "version": "https://registry.npmjs.org/pff/-/pff-1.0.0.tgz", + "integrity": "sha1-6l8J7mVxyuKSp4/CgJBaOGVmjng=", + "dev": true + }, + "pify": { + "version": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "pinkie": { + "version": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true + }, + "pkg-dir": { + "version": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-1.0.0.tgz", + "integrity": "sha1-ektQio1bstYp1EcFb/TpyTFM89Q=", + "dev": true + }, + "pluralize": { + "version": "https://registry.npmjs.org/pluralize/-/pluralize-1.2.1.tgz", + "integrity": "sha1-0aIUg/0iu0HlihL6NCGCMUCJfEU=", + "dev": true + }, + "prelude-ls": { + "version": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "private": { + "version": "https://registry.npmjs.org/private/-/private-0.1.7.tgz", + "integrity": "sha1-aM5eih7woju1cMwoU3tTMqumPvE=", + "dev": true + }, + "process": { + "version": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true + }, + "process-nextick-args": { + "version": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" + }, + "progress": { + "version": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", + "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", + "dev": true + }, + "proxyquireify": { + "version": "https://registry.npmjs.org/proxyquireify/-/proxyquireify-3.2.1.tgz", + "integrity": "sha1-Fb7hATYKzJHc2G7k2aRF+Klx7qA=", + "dev": true, + "dependencies": { + "acorn": { + "version": "https://registry.npmjs.org/acorn/-/acorn-1.2.2.tgz", + "integrity": "sha1-yM4n3grMdtiW0rH6099YjZ6C8BQ=", + "dev": true + }, + "detective": { + "version": "https://registry.npmjs.org/detective/-/detective-4.1.1.tgz", + "integrity": "sha1-nEusHp+4uzT38YyuCA6h0Dr/LNo=", + "dev": true + }, + "through": { + "version": "https://registry.npmjs.org/through/-/through-2.2.7.tgz", + "integrity": "sha1-bo4hIAGR1OtqmfbwEN9Gqhxusr0=", + "dev": true + }, + "xtend": { + "version": "https://registry.npmjs.org/xtend/-/xtend-3.0.0.tgz", + "integrity": "sha1-XM50B7r2Qsunvs2laBEcST9ZZlo=", + "dev": true + } + } + }, + "public-encrypt": { + "version": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.0.tgz", + "integrity": "sha1-OfaZ86RlYN1eusvKaTyvfGXBjMY=", + "dev": true + }, + "punycode": { + "version": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "querystring": { + "version": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "dev": true + }, + "querystring-es3": { + "version": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", + "dev": true + }, + "randombytes": { + "version": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.4.tgz", + "integrity": "sha1-lVHfIIQiyPgOtY4jJt0LhA/yLv0=", + "dev": true + }, + "read-only-stream": { + "version": "https://registry.npmjs.org/read-only-stream/-/read-only-stream-2.0.0.tgz", + "integrity": "sha1-JyT9aoET1zdkrCiNQ4YnDB2/F/A=", + "dev": true + }, + "read-pkg": { + "version": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "dev": true + }, + "read-pkg-up": { + "version": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "dev": true, + "dependencies": { + "find-up": { + "version": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true + } + } + }, + "readable-stream": { + "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.10.tgz", + "integrity": "sha1-7/5yu3yITA3TNeI3nVJhltnQEe4=", + "dev": true, + "dependencies": { + "string_decoder": { + "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.1.tgz", + "integrity": "sha1-YuIA8DmVWmgQ2N8KM//A8BNmLZg=", + "dev": true + } + } + }, + "readline2": { + "version": "https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz", + "integrity": "sha1-QQWWCP/BVHV7cV2ZidGZ/783LjU=", + "dev": true + }, + "rechoir": { + "version": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "dev": true + }, + "regenerate": { + "version": "https://registry.npmjs.org/regenerate/-/regenerate-1.3.2.tgz", + "integrity": "sha1-0ZQcZ7rUN+G+dkM63Vs4X5WxkmA=", + "dev": true + }, + "regenerator-runtime": { + "version": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", + "integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg=", + "dev": true + }, + "regenerator-transform": { + "version": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.9.11.tgz", + "integrity": "sha1-On0GdSDLe3F2dp61/4aGkb7+EoM=", + "dev": true + }, + "regexp-clone": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-0.0.1.tgz", + "integrity": "sha1-p8LgmJH9vzj7sQ03b7cwA+aKxYk=" + }, + "regexpu-core": { + "version": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-2.0.0.tgz", + "integrity": "sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA=", + "dev": true + }, + "regjsgen": { + "version": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", + "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=", + "dev": true + }, + "regjsparser": { + "version": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", + "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", + "dev": true + }, + "repeating": { + "version": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "dev": true + }, + "require_optional": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", + "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", + "dependencies": { + "resolve-from": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", + "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" + } + } + }, + "require-deps": { + "version": "https://registry.npmjs.org/require-deps/-/require-deps-1.0.1.tgz", + "integrity": "sha1-JBXPScNb02pdMXc5UQjT8jcgUmM=", + "dev": true + }, + "require-uncached": { + "version": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", + "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", + "dev": true + }, + "resolve": { + "version": "https://registry.npmjs.org/resolve/-/resolve-1.3.3.tgz", + "integrity": "sha1-ZVkHw0aahoDcLeOidaj91paR8OU=", + "dev": true + }, + "resolve-from": { + "version": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", + "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", + "dev": true + }, + "restore-cursor": { + "version": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", + "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", + "dev": true + }, + "rimraf": { + "version": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.1.tgz", + "integrity": "sha1-wjOOxkPfeht/5cVPqG9XQopV8z0=", + "dev": true + }, + "ripemd160": { + "version": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.1.tgz", + "integrity": "sha1-D0WEKVxTo2KK9+bXmsohzlfRxuc=", + "dev": true + }, + "run-async": { + "version": "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz", + "integrity": "sha1-yK1KXhEGYeQCp9IbUw4AnyX444k=", + "dev": true + }, + "rx-lite": { + "version": "https://registry.npmjs.org/rx-lite/-/rx-lite-3.1.2.tgz", + "integrity": "sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI=", + "dev": true + }, + "safe-buffer": { + "version": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.0.1.tgz", + "integrity": "sha1-0mPKVGls2KMGtcplUekt5XkY++c=", + "dev": true + }, + "samsam": { + "version": "https://registry.npmjs.org/samsam/-/samsam-1.1.2.tgz", + "integrity": "sha1-vsEf3IOp/aBjQBIQ5AF2wwJNFWc=" + }, + "select": { + "version": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", + "integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=" + }, + "semver": { + "version": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=" + }, + "sha.js": { + "version": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.8.tgz", + "integrity": "sha1-NwaMLEdra69ALRSknGf1l5IfY08=", + "dev": true + }, + "shasum": { + "version": "https://registry.npmjs.org/shasum/-/shasum-1.0.2.tgz", + "integrity": "sha1-5wEjENj0F/TetXEhUOVni4euVl8=", + "dev": true + }, + "shell-quote": { + "version": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz", + "integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=", + "dev": true + }, + "shelljs": { + "version": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.7.tgz", + "integrity": "sha1-svXHfvlxSPS09uImguELuoZnz/E=", + "dev": true + }, + "sinon": { + "version": "https://registry.npmjs.org/sinon/-/sinon-1.17.7.tgz", + "integrity": "sha1-RUKk9JugxFwF6y6d2dID4rjv4L8=" + }, + "slash": { + "version": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", + "dev": true + }, + "slice-ansi": { + "version": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", + "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", + "dev": true + }, + "sliced": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", + "integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E=" + }, + "source-map": { + "version": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI=" + }, + "source-map-support": { + "version": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.15.tgz", + "integrity": "sha1-AyAt9lwG0r2MfsI2KhkwVv7407E=", + "dev": true + }, + "spdx-correct": { + "version": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz", + "integrity": "sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=", + "dev": true + }, + "spdx-expression-parse": { + "version": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz", + "integrity": "sha1-m98vIOH0DtRH++JzJmGR/O1RYmw=", + "dev": true + }, + "spdx-license-ids": { + "version": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz", + "integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=", + "dev": true + }, + "sprintf-js": { + "version": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "statuses": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", + "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" + }, + "stream-browserify": { + "version": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", + "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=", + "dev": true + }, + "stream-combiner2": { + "version": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", + "integrity": "sha1-+02KFCDqNidk4hrUeAOXvry0HL4=", + "dev": true + }, + "stream-http": { + "version": "https://registry.npmjs.org/stream-http/-/stream-http-2.7.1.tgz", + "integrity": "sha1-VGpRdBrVprB+njGwsQRBqRffUoo=", + "dev": true + }, + "stream-splicer": { + "version": "https://registry.npmjs.org/stream-splicer/-/stream-splicer-2.0.0.tgz", + "integrity": "sha1-G2O+Q4oTPktnHMGTUZdgAXWRDYM=", + "dev": true + }, + "string_decoder": { + "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "string-width": { + "version": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true + }, + "strip-ansi": { + "version": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true + }, + "strip-bom": { + "version": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-json-comments": { + "version": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, + "stround": { + "version": "https://registry.npmjs.org/stround/-/stround-0.3.1.tgz", + "integrity": "sha1-vl8uHdf1tqFGhoJUish0YbjWZFQ=" + }, + "subarg": { + "version": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", + "integrity": "sha1-9izxdYHplrSPyWVpn1TAauJouNI=", + "dev": true, + "dependencies": { + "minimist": { + "version": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "supports-color": { + "version": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, + "syntax-error": { + "version": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.3.0.tgz", + "integrity": "sha1-HtkmbE1AvnXcVb+bsct3Biu5bKE=", + "dev": true + }, + "tabbable": { + "version": "https://registry.npmjs.org/tabbable/-/tabbable-1.0.6.tgz", + "integrity": "sha1-fCaofqb0ol7fXtthl0WgrnQHJPw=" + }, + "table": { + "version": "https://registry.npmjs.org/table/-/table-3.8.3.tgz", + "integrity": "sha1-K7xULw/amGGnVdOUf+/Ys/UThV8=", + "dev": true, + "dependencies": { + "is-fullwidth-code-point": { + "version": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "https://registry.npmjs.org/string-width/-/string-width-2.0.0.tgz", + "integrity": "sha1-Y1xUNsxypuDDh87KJ41OLuxSaH4=", + "dev": true + } + } + }, + "text-table": { + "version": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "through": { + "version": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "through2": { + "version": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", + "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", + "dev": true + }, + "timers-browserify": { + "version": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-1.4.2.tgz", + "integrity": "sha1-ycWLV1voQHN1y14kYtrO50NZ9B0=", + "dev": true + }, + "tiny-emitter": { + "version": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.0.0.tgz", + "integrity": "sha1-utMnrbGAS0KiMa+nQVMr2ITNCa0=" + }, + "to-arraybuffer": { + "version": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", + "dev": true + }, + "to-fast-properties": { + "version": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", + "dev": true + }, + "trim-right": { + "version": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", + "dev": true + }, + "tryit": { + "version": "https://registry.npmjs.org/tryit/-/tryit-1.0.3.tgz", + "integrity": "sha1-OTvnMKlEb9Hq1tpZoBQwjzbCics=", + "dev": true + }, + "tty-browserify": { + "version": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", + "dev": true + }, + "type-check": { + "version": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true + }, + "type-detect": { + "version": "https://registry.npmjs.org/type-detect/-/type-detect-1.0.0.tgz", + "integrity": "sha1-diIXzAbbJY7EiQihKY6LlRIejqI=", + "dev": true + }, + "typedarray": { + "version": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "uglify-js": { + "version": "3.0.25", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.0.25.tgz", + "integrity": "sha512-JO1XE0WZ9m6UpDkN7WCyPNAWI6EN3K0g40ekcoJKejViYmryJ0BaLxXjvra1IsAeIlJfq72scTbhl0jknsT2GA==" + }, + "umd": { + "version": "https://registry.npmjs.org/umd/-/umd-3.0.1.tgz", + "integrity": "sha1-iuVW4RAR9jwllnCKiDclnwGz1g4=", + "dev": true + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "url": { + "version": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dev": true, + "dependencies": { + "punycode": { + "version": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + } + } + }, + "user-home": { + "version": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz", + "integrity": "sha1-nHC/2Babwdy/SGBODwS4tJzenp8=", + "dev": true + }, + "util": { + "version": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=" + }, + "util-deprecate": { + "version": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "utils-merge": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz", + "integrity": "sha1-ApT7kiu5N1FTVBxPcJYjHyh8ivg=" + }, + "validate-npm-package-license": { + "version": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz", + "integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=", + "dev": true + }, + "vm-browserify": { + "version": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", + "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=", + "dev": true + }, + "wordwrap": { + "version": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + }, + "wrappy": { + "version": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write": { + "version": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", + "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", + "dev": true + }, + "xtend": { + "version": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", + "dev": true + }, + "zxcvbn": { + "version": "https://registry.npmjs.org/zxcvbn/-/zxcvbn-4.4.2.tgz", + "integrity": "sha1-KOwXzwl0PtyrBW3dixsGJizHPDA=" + } + } +} diff --git a/package.json b/package.json index eb1a5e66334..5e216e62d1e 100644 --- a/package.json +++ b/package.json @@ -7,12 +7,14 @@ "npm": "~3.8.x" }, "dependencies": { + "app": "^0.1.0", "basscss-sass": "^3.0.0", "classlist.js": "^1.1.20150312", "clipboard": "^1.6.1", "field-kit": "^2.1.0", "focus-trap": "^2.3.0", "hint.css": "^2.3.2", + "libphonenumber-js": "^0.4.23", "normalize.css": "^4.2.0", "sinon": "^1.17.7", "zxcvbn": "^4.4.2" diff --git a/scripts/i18n-csv-to-yml b/scripts/i18n-csv-to-yml new file mode 100755 index 00000000000..b92be1bffc4 --- /dev/null +++ b/scripts/i18n-csv-to-yml @@ -0,0 +1,78 @@ +#!/usr/bin/env ruby +require 'optparse' +require 'csv' +require 'yaml' +require 'fileutils' + +Config = Struct.new(:out_dir, :out_locale, :in_csv, :separate_files, :dry_run) + +config = Config.new + +parser = OptionParser.new do |opts| + opts.banner = "Usage: #{$PROGRAM_NAME} [OPTIONS]" + + opts.on('--out-dir=OUT_DIR', 'The directory to write to') do |out_dir| + config.out_dir = out_dir + end + + opts.on('--out-locale=OUT_LOCALE', 'The locale to export') do |out_locale| + config.out_locale = out_locale.downcase + end + + opts.on('--in-csv=IN_CSV', 'The CSV to read translations from') do |in_csv| + config.in_csv = in_csv + end + + opts.on('--separate', 'Write separate .yml files per top-level key') do + config.separate_files = true + end + + opts.on('--dry-run', 'Do not write to the filesystem') do + config.dry_run = true + end + + opts.on('--help', '-h', 'Show this message') do + puts opts + end +end + +parser.parse(ARGV) + +if !config.out_dir || !config.out_locale || !config.in_csv + puts parser + exit 1 +end + +# hash where keys always initialize to a empty hashes (with the same property) +hash = ->(f) { f[f] }[->(f) { Hash.new { |h, k| h[k] = f[f] } }] + +csv = CSV.read(config.in_csv, headers: true) + +def set_nested(hash, key, value) + *parts, last = key.split('.') + parts.each do |part| + hash = hash[part] + end + hash[last] = value +end + +csv.each do |row| + set_nested(hash, row['Key'], row[config.out_locale.upcase]) +end + +if config.separate_files + hash.each do |top_level_key, values| + path = File.join(config.out_dir, top_level_key, "#{config.out_locale}.yml") + puts path + + content = { config.out_locale => { top_level_key => values } } + FileUtils.mkdir_p(File.dirname(path)) + File.open(path, 'w') { |f| f.puts content.to_yaml } unless config.dry_run + end +else + path = File.join(config.out_dir, "#{config.out_locale}.yml") + puts path + + content = { config.out_locale => hash } + File.open(path, 'w') { |f| f.puts content.to_yaml } unless config.dry_run +end diff --git a/scripts/normalize-yaml b/scripts/normalize-yaml new file mode 100755 index 00000000000..8a37da93dd4 --- /dev/null +++ b/scripts/normalize-yaml @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby +$LOAD_PATH.unshift(File.expand_path('../lib', File.dirname(__FILE__))) +require 'yaml_normalizer' +require 'yaml' + +YamlNormalizer.run(ARGV) diff --git a/spec/controllers/saml_idp_controller_spec.rb b/spec/controllers/saml_idp_controller_spec.rb index f6fe4d29e6d..025cf9ed02a 100644 --- a/spec/controllers/saml_idp_controller_spec.rb +++ b/spec/controllers/saml_idp_controller_spec.rb @@ -306,8 +306,8 @@ saml_get_auth(saml_settings) end - it 'deletes SP metadata from session' do - expect(session.key?(:sp)).to eq(false) + it 'does not delete SP metadata from session' do + expect(session.key?(:sp)).to eq(true) end end end diff --git a/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb b/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb index f7c3b1afcf2..27cd54733b8 100644 --- a/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb +++ b/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb @@ -380,6 +380,10 @@ expect(subject.user_session[:idv][:params]['phone_confirmed_at']).to_not be_nil end + it 'updates idv session user_phone_confirmation attributes' do + expect(subject.user_session[:idv][:user_phone_confirmation]).to eq(true) + end + it 'does not update user phone attributes' do expect(subject.current_user.reload.phone).to eq '+1 (202) 555-1212' expect(subject.current_user.reload.phone_confirmed_at).to eq @previous_phone_confirmed_at diff --git a/spec/controllers/two_factor_authentication/totp_verification_controller_spec.rb b/spec/controllers/two_factor_authentication/totp_verification_controller_spec.rb index d55bb7d82bf..bed129a66c4 100644 --- a/spec/controllers/two_factor_authentication/totp_verification_controller_spec.rb +++ b/spec/controllers/two_factor_authentication/totp_verification_controller_spec.rb @@ -6,7 +6,7 @@ before do sign_in_before_2fa @secret = subject.current_user.generate_totp_secret - subject.current_user.otp_secret_key = @secret + subject.current_user.update(otp_secret_key: @secret) end it 'redirects to the profile' do @@ -127,5 +127,25 @@ end end end + + context 'when the user does not have an authenticator app enabled' do + it 'redirects to user_two_factor_authentication_path' do + stub_sign_in_before_2fa + post :create, code: '123456' + + expect(response).to redirect_to user_two_factor_authentication_path + end + end + end + + describe '#show' do + context 'when the user does not have an authenticator app enabled' do + it 'redirects to user_two_factor_authentication_path' do + stub_sign_in_before_2fa + get :show + + expect(response).to redirect_to user_two_factor_authentication_path + end + end end end diff --git a/spec/controllers/users/phones_controller_spec.rb b/spec/controllers/users/phones_controller_spec.rb index be1dc4f75ec..c4dedc1d413 100644 --- a/spec/controllers/users/phones_controller_spec.rb +++ b/spec/controllers/users/phones_controller_spec.rb @@ -16,7 +16,7 @@ stub_analytics allow(@analytics).to receive(:track_event) - put :update, update_user_phone_form: { phone: new_phone } + put :update, update_user_phone_form: { phone: new_phone, international_code: 'US' } end it 'lets user know they need to confirm their new phone' do @@ -37,7 +37,7 @@ it 'does not delete the phone' do stub_sign_in(user) - put :update, update_user_phone_form: { phone: '' } + put :update, update_user_phone_form: { phone: '', international_code: 'US' } expect(user.reload.phone).to be_present expect(response).to render_template(:edit) @@ -51,7 +51,7 @@ stub_analytics allow(@analytics).to receive(:track_event) - put :update, update_user_phone_form: { phone: second_user.phone } + put :update, update_user_phone_form: { phone: second_user.phone, international_code: 'US' } end it 'processes successfully and informs user' do @@ -74,7 +74,7 @@ user = build(:user, phone: '123-123-1234') stub_sign_in(user) - put :update, update_user_phone_form: { phone: invalid_phone } + put :update, update_user_phone_form: { phone: invalid_phone, international_code: 'US' } expect(user.phone).not_to eq invalid_phone expect(response).to render_template(:edit) @@ -85,7 +85,7 @@ it 'redirects to profile page without any messages' do stub_sign_in(user) - put :update, update_user_phone_form: { phone: user.phone } + put :update, update_user_phone_form: { phone: user.phone, international_code: 'US' } expect(response).to redirect_to account_url expect(flash.keys).to be_empty diff --git a/spec/controllers/users/sessions_controller_spec.rb b/spec/controllers/users/sessions_controller_spec.rb index 9be166701c8..b981880208e 100644 --- a/spec/controllers/users/sessions_controller_spec.rb +++ b/spec/controllers/users/sessions_controller_spec.rb @@ -323,5 +323,22 @@ get :new end end + + context 'with fully authenticated user who has a pending profile' do + it 'redirects to the verify profile page' do + profile = create( + :profile, + deactivation_reason: :verification_pending, + phone_confirmed: false, + pii: { otp: 'abc123', ssn: '6666', dob: '1920-01-01' } + ) + user = profile.user + + stub_sign_in(user) + get :new + + expect(response).to redirect_to verify_account_path + end + end end end diff --git a/spec/controllers/users/two_factor_authentication_controller_spec.rb b/spec/controllers/users/two_factor_authentication_controller_spec.rb index 26b9618fa99..0683fd5e659 100644 --- a/spec/controllers/users/two_factor_authentication_controller_spec.rb +++ b/spec/controllers/users/two_factor_authentication_controller_spec.rb @@ -141,6 +141,8 @@ def index otp_delivery_preference: 'sms', resend: nil, context: 'authentication', + country_code: '1', + area_code: '202', } expect(@analytics).to receive(:track_event). @@ -203,6 +205,8 @@ def index otp_delivery_preference: 'voice', resend: nil, context: 'authentication', + country_code: '1', + area_code: '202', } expect(@analytics).to receive(:track_event). diff --git a/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb b/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb index c1700284d02..595764589f4 100644 --- a/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb +++ b/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb @@ -28,7 +28,11 @@ expect(@analytics).to receive(:track_event). with(Analytics::MULTI_FACTOR_AUTH_PHONE_SETUP, result) - patch :set, two_factor_setup_form: { phone: '703-555-010', otp_delivery_preference: :sms } + patch :set, two_factor_setup_form: { + phone: '703-555-010', + otp_delivery_preference: :sms, + international_code: 'US', + } expect(response).to render_template(:index) end @@ -50,7 +54,8 @@ patch( :set, two_factor_setup_form: { phone: '703-555-0100', - otp_delivery_preference: 'voice' } + otp_delivery_preference: 'voice', + international_code: 'US' } ) expect(response).to redirect_to( @@ -81,7 +86,8 @@ patch( :set, two_factor_setup_form: { phone: '703-555-0100', - otp_delivery_preference: :sms } + otp_delivery_preference: :sms, + international_code: 'US' } ) expect(response).to redirect_to( @@ -110,7 +116,9 @@ patch( :set, - two_factor_setup_form: { phone: '703-555-0100', otp_delivery_preference: :sms } + two_factor_setup_form: { phone: '703-555-0100', + otp_delivery_preference: :sms, + international_code: 'US' } ) expect(response).to redirect_to( diff --git a/spec/controllers/users/verify_password_controller_spec.rb b/spec/controllers/users/verify_password_controller_spec.rb index 540ce2967a7..fba5173e971 100644 --- a/spec/controllers/users/verify_password_controller_spec.rb +++ b/spec/controllers/users/verify_password_controller_spec.rb @@ -20,71 +20,72 @@ end end - context 'without personal key flag set' do - let(:profiles) { [create(:profile, deactivation_reason: :password_reset)] } - - describe '#new' do - it 'redirects to the root url' do - get :new - expect(response).to redirect_to(root_url) - end - end - - describe '#update' do - it 'redirects to the root url' do - get :new - expect(response).to redirect_to(root_url) - end - end - end - context 'with password reset profile' do let(:profiles) { [create(:profile, deactivation_reason: :password_reset)] } let(:response_ok) { FormResponse.new(success: true, errors: {}, extra: { personal_key: key }) } let(:response_bad) { FormResponse.new(success: false, errors: {}) } let(:key) { 'key' } - before do - allow(subject.reactivate_account_session).to receive(:personal_key?).and_return(personal_key) - end - - describe '#new' do - it 'renders the `new` template' do - get :new + context 'without personal key flag set' do + describe '#new' do + it 'redirects to the root url' do + get :new + expect(response).to redirect_to(root_url) + end + end - expect(response).to render_template(:new) + describe '#update' do + it 'redirects to the root url' do + get :new + expect(response).to redirect_to(root_url) + end end end - describe '#update' do - let(:form) { instance_double(VerifyPasswordForm) } - + context 'with personal key flag set' do before do - expect(controller).to receive(:verify_password_form).and_return(form) + allow(subject.reactivate_account_session).to receive(:personal_key?). + and_return(personal_key) end - context 'with valid password' do - before do - allow(form).to receive(:submit).and_return(response_ok) - put :update, user: { password: user.password } + describe '#new' do + it 'renders the `new` template' do + get :new + + expect(response).to render_template(:new) end + end - it 'redirects to the account page' do - expect(response).to redirect_to(account_url) + describe '#update' do + let(:form) { instance_double(VerifyPasswordForm) } + + before do + expect(controller).to receive(:verify_password_form).and_return(form) end - it 'sets a new personal key as a flash message' do - expect(flash[:personal_key]).to eq(key) + context 'with valid password' do + before do + allow(form).to receive(:submit).and_return(response_ok) + put :update, user: { password: user.password } + end + + it 'redirects to the account page' do + expect(response).to redirect_to(account_url) + end + + it 'sets a new personal key as a flash message' do + expect(flash[:personal_key]).to eq(key) + end end - end - context 'without valid password' do - it 'renders the new template' do - allow(form).to receive(:submit).and_return(response_bad) + context 'without valid password' do + it 'renders the new template' do + allow(form).to receive(:submit).and_return(response_bad) - put :update, user: { password: user.password } + put :update, user: { password: user.password } - expect(response).to render_template(:new) + expect(response).to render_template(:new) + end end end end diff --git a/spec/controllers/users/verify_personal_key_controller_spec.rb b/spec/controllers/users/verify_personal_key_controller_spec.rb index 0e16e56d7ca..d8d7698bfa3 100644 --- a/spec/controllers/users/verify_personal_key_controller_spec.rb +++ b/spec/controllers/users/verify_personal_key_controller_spec.rb @@ -33,7 +33,7 @@ it 'displays a flash message to the user' do get :new - expect(subject.flash[:notice]).to eq(t('notices.account_recovery')) + expect(subject.flash[:notice]).to eq(t('notices.account_reactivation')) end end end diff --git a/spec/controllers/verify/address_controller_spec.rb b/spec/controllers/verify/address_controller_spec.rb index a590ed04d37..5c271db6326 100644 --- a/spec/controllers/verify/address_controller_spec.rb +++ b/spec/controllers/verify/address_controller_spec.rb @@ -9,7 +9,7 @@ describe '#index' do it 'redirects if phone mechanism selected' do - subject.idv_session.phone_confirmation = true + subject.idv_session.vendor_phone_confirmation = true get :index diff --git a/spec/controllers/verify/confirmations_controller_spec.rb b/spec/controllers/verify/confirmations_controller_spec.rb index a86c74994e5..81bf90c13a3 100644 --- a/spec/controllers/verify/confirmations_controller_spec.rb +++ b/spec/controllers/verify/confirmations_controller_spec.rb @@ -66,7 +66,10 @@ def stub_idv_session context 'user used 2FA phone as phone of record' do before do - subject.idv_session.phone_confirmation = true + subject.idv_session.params['phone'] = user.phone + subject.idv_session.params['phone_confirmed_at'] = Time.zone.now + subject.idv_session.vendor_phone_confirmation = true + subject.idv_session.user_phone_confirmation = true end it 'activates profile' do @@ -148,6 +151,7 @@ def stub_idv_session context 'user confirmed a new phone' do it 'tracks that event' do stub_analytics + subject.idv_session.params['phone'] = '+1 (202) 555-9876' subject.idv_session.params['phone_confirmed_at'] = Time.zone.now result = { diff --git a/spec/controllers/verify/finance_controller_spec.rb b/spec/controllers/verify/finance_controller_spec.rb index 4ae70d9091c..8e90712fbcc 100644 --- a/spec/controllers/verify/finance_controller_spec.rb +++ b/spec/controllers/verify/finance_controller_spec.rb @@ -80,12 +80,119 @@ end end end + + it 'tracks the form errors and does not make a vendor API call' do + stub_analytics + allow(@analytics).to receive(:track_event) + + allow(Idv::FinancialsValidator).to receive(:new) + + put :create, idv_finance_form: { finance_type: :ccn, ccn: '123' } + + result = { + success: false, + errors: { ccn: ['Credit card number should be only last 8 digits.'] }, + } + + expect(@analytics).to have_received(:track_event). + with(Analytics::IDV_FINANCE_CONFIRMATION_FORM, result) + expect(subject.idv_session.financials_confirmation).to be_falsy + expect(Idv::FinancialsValidator).to_not have_received(:new) + end end context 'when form is valid' do + it 'redirects to the show page' do + put :create, idv_finance_form: { finance_type: :ccn, ccn: '12345678' } + + expect(response).to redirect_to(verify_finance_result_path) + end + + it 'tracks the successful submission with no errors' do + stub_analytics + allow(@analytics).to receive(:track_event) + + put :create, idv_finance_form: { finance_type: :ccn, ccn: '12345678' } + + result = { + success: true, + errors: {}, + } + + expect(@analytics).to have_received(:track_event).with( + Analytics::IDV_FINANCE_CONFIRMATION_FORM, result + ) + end + end + end + + describe '#show' do + before do + stub_subject + end + + context 'when the background job is not complete yet' do + render_views + + it 'renders a spinner and has the page refresh' do + get :show + + expect(response).to render_template('shared/refresh') + + dom = Nokogiri::HTML(response.body) + expect(dom.css('meta[http-equiv="refresh"]')).to be_present + end + end + + context 'when the background job has timed out' do + let(:expired_started_at) do + Time.zone.now.to_i - Figaro.env.async_job_refresh_max_wait_seconds.to_i + end + + before do + controller.idv_session.async_result_started_at = expired_started_at + controller.idv_session.params = { ccn: '12345678' } + end + + it 'displays an error' do + get :show + + expect(response).to render_template :new + expect(flash[:warning]).to include(t('idv.modal.financials.timeout')) + end + + it 'tracks the failure as a timeout' do + stub_analytics + allow(@analytics).to receive(:track_event) + + get :show + + result = { + success: false, + errors: { timed_out: ['Timed out waiting for vendor response'] }, + } + + expect(@analytics).to have_received(:track_event).with( + Analytics::IDV_FINANCE_CONFIRMATION_VENDOR, result + ) + end + end + + context 'when the background job has completed' do + let(:result_id) { SecureRandom.uuid } + + before do + controller.idv_session.async_result_id = result_id + VendorValidatorResultStorage.new.store(result_id: result_id, result: result) + end + context 'when CCN is confirmed' do + let(:result) { Idv::VendorResult.new(success: true) } + it 'redirects to phone page' do - put :create, idv_finance_form: { finance_type: :ccn, ccn: '12345678' } + controller.idv_session.params = { ccn: '12345678' } + + get :show expect(flash[:success]).to eq(t('idv.messages.personal_details_verified')) expect(response).to redirect_to verify_address_url @@ -93,19 +200,62 @@ expected_params = { ccn: '12345678' } expect(subject.idv_session.params).to eq expected_params end + + it 'tracks the successful submission with no errors' do + stub_analytics + allow(@analytics).to receive(:track_event) + + get :show + + result = { + success: true, + errors: {}, + } + + expect(@analytics).to have_received(:track_event).with( + Analytics::IDV_FINANCE_CONFIRMATION_VENDOR, result + ) + end end context 'when CCN is not confirmed' do + let(:result) do + Idv::VendorResult.new( + success: false, + errors: { ccn: ['The ccn could not be verified.'] } + ) + end + + before do + controller.idv_session.params = { finance_type: 'ccn', ccn: '00000000' } + end + it 'renders #new with error' do - put :create, idv_finance_form: { finance_type: :ccn, ccn: '00000000' } + get :show expect(flash[:warning]).to match t('idv.modal.financials.heading') expect(flash[:warning]).to match t('idv.modal.attempts', count: max_attempts - 1) expect(response).to render_template :new end + + it 'tracks the vendor error' do + stub_analytics + allow(@analytics).to receive(:track_event) + + get :show + + result = { + success: false, + errors: { ccn: ['The ccn could not be verified.'] }, + } + + expect(@analytics).to have_received(:track_event). + with(Analytics::IDV_FINANCE_CONFIRMATION_VENDOR, result) + end end context 'attempt window has expired, previous attempts == max-1' do + let(:result) { Idv::VendorResult.new(success: true) } let(:two_days_ago) { Time.zone.now - 2.days } before do @@ -114,7 +264,7 @@ end it 'allows and does not affect attempt counter' do - put :create, idv_finance_form: { finance_type: :ccn, ccn: '12345678' } + get :show expect(response).to redirect_to verify_address_path expect(subject.current_user.idv_attempts).to eq(max_attempts - 1) @@ -124,61 +274,6 @@ end end - describe 'analytics' do - before do - stub_subject - stub_analytics - allow(@analytics).to receive(:track_event) - end - - context 'when form is valid and CCN passes vendor validation' do - it 'tracks the successful submission with no errors' do - put :create, idv_finance_form: { finance_type: :ccn, ccn: '12345678' } - - result = { - success: true, - errors: {}, - } - - expect(@analytics).to have_received(:track_event).with( - Analytics::IDV_FINANCE_CONFIRMATION, result - ) - end - end - - context 'when the form is valid but the CCN does not pass vendor validation' do - it 'tracks the vendor error' do - put :create, idv_finance_form: { finance_type: :ccn, ccn: '00000000' } - - result = { - success: false, - errors: { ccn: ['The ccn could not be verified.'] }, - } - - expect(@analytics).to have_received(:track_event). - with(Analytics::IDV_FINANCE_CONFIRMATION, result) - end - end - - context 'when the form is invalid' do - it 'tracks the form errors and does not make a vendor API call' do - allow(Idv::FinancialsValidator).to receive(:new) - - put :create, idv_finance_form: { finance_type: :ccn, ccn: '123' } - - result = { - success: false, - errors: { ccn: ['Credit card number should be only last 8 digits.'] }, - } - - expect(@analytics).to have_received(:track_event). - with(Analytics::IDV_FINANCE_CONFIRMATION, result) - expect(subject.idv_session.financials_confirmation).to eq false - expect(Idv::FinancialsValidator).to_not have_received(:new) - end - end - end - def stub_subject user = stub_sign_in idv_session = Idv::Session.new( diff --git a/spec/controllers/verify/phone_controller_spec.rb b/spec/controllers/verify/phone_controller_spec.rb index 5f20d7df012..c94a9742f3f 100644 --- a/spec/controllers/verify/phone_controller_spec.rb +++ b/spec/controllers/verify/phone_controller_spec.rb @@ -1,9 +1,11 @@ require 'rails_helper' -include Features::LocalizationHelper describe Verify::PhoneController do + include Features::LocalizationHelper + let(:max_attempts) { Idv::Attempter.idv_max_attempts } let(:good_phone) { '+1 (555) 555-0000' } + let(:normalized_phone) { '5555550000' } let(:bad_phone) { '+1 (555) 555-5555' } describe 'before_actions' do @@ -24,7 +26,7 @@ end it 'redirects to review when step is complete' do - subject.idv_session.phone_confirmation = true + subject.idv_session.vendor_phone_confirmation = true get :new @@ -50,16 +52,16 @@ end it 'renders #new' do - put :create, idv_phone_form: { phone: '703' } + put :create, idv_phone_form: { phone: '703', international_code: 'US' } expect(flash[:warning]).to be_nil expect(subject.idv_session.params).to be_empty end it 'tracks form error and does not make a vendor API call' do - allow(Idv::PhoneValidator).to receive(:new) + expect(Idv::PhoneValidator).to_not receive(:new) - put :create, idv_phone_form: { phone: '703' } + put :create, idv_phone_form: { phone: '703', international_code: 'US' } result = { success: false, @@ -69,10 +71,9 @@ } expect(@analytics).to have_received(:track_event).with( - Analytics::IDV_PHONE_CONFIRMATION, result + Analytics::IDV_PHONE_CONFIRMATION_FORM, result ) - expect(subject.idv_session.phone_confirmation).to eq false - expect(Idv::PhoneValidator).to_not have_received(:new) + expect(subject.idv_session.vendor_phone_confirmation).to be_falsy end end @@ -86,46 +87,26 @@ user = build(:user, phone: good_phone, phone_confirmed_at: Time.zone.now) stub_verify_steps_one_and_two(user) - put :create, idv_phone_form: { phone: good_phone } + put :create, idv_phone_form: { phone: good_phone, international_code: 'US' } result = { success: true, errors: {} } expect(@analytics).to have_received(:track_event).with( - Analytics::IDV_PHONE_CONFIRMATION, result - ) - end - - it 'tracks event with invalid phone' do - user = build(:user, phone: bad_phone, phone_confirmed_at: Time.zone.now) - stub_verify_steps_one_and_two(user) - - put :create, idv_phone_form: { phone: bad_phone } - - result = { - success: false, - errors: { - phone: ['The phone number could not be verified.'], - }, - } - - expect(flash[:warning]).to match t('idv.modal.phone.heading') - expect(flash[:warning]).to match t('idv.modal.attempts', count: max_attempts - 1) - expect(@analytics).to have_received(:track_event).with( - Analytics::IDV_PHONE_CONFIRMATION, result + Analytics::IDV_PHONE_CONFIRMATION_FORM, result ) end context 'when same as user phone' do - it 'redirects to review page and sets phone_confirmed_at' do + it 'redirects to result page and sets phone_confirmed_at' do user = build(:user, phone: good_phone, phone_confirmed_at: Time.zone.now) stub_verify_steps_one_and_two(user) - put :create, idv_phone_form: { phone: good_phone } + put :create, idv_phone_form: { phone: good_phone, international_code: 'US' } - expect(response).to redirect_to verify_review_path + expect(response).to redirect_to verify_phone_result_path expected_params = { - phone: good_phone, + phone: normalized_phone, phone_confirmed_at: user.phone_confirmed_at, } expect(subject.idv_session.params).to eq expected_params @@ -133,39 +114,150 @@ end context 'when different from user phone' do - it 'redirects to review page and does not set phone_confirmed_at' do + it 'redirects to result page and does not set phone_confirmed_at' do user = build(:user, phone: '+1 (415) 555-0130', phone_confirmed_at: Time.zone.now) stub_verify_steps_one_and_two(user) - put :create, idv_phone_form: { phone: good_phone } + put :create, idv_phone_form: { phone: good_phone, international_code: 'US' } - expect(response).to redirect_to verify_review_path + expect(response).to redirect_to verify_phone_result_path expected_params = { - phone: good_phone, + phone: normalized_phone, } expect(subject.idv_session.params).to eq expected_params end end + end + end + + describe '#show' do + let(:user) { build(:user, phone: good_phone, phone_confirmed_at: Time.zone.now) } + let(:params) { { phone: good_phone } } + + before do + stub_verify_steps_one_and_two(user) + controller.idv_session.params = params + end + + context 'when the background job is not complete yet' do + render_views + + it 'renders a spinner and has the page refresh' do + get :show + + expect(response).to render_template('shared/refresh') + + dom = Nokogiri::HTML(response.body) + expect(dom.css('meta[http-equiv="refresh"]')).to be_present + end + end + + context 'when the background job has timed out' do + let(:expired_started_at) do + Time.zone.now.to_i - Figaro.env.async_job_refresh_max_wait_seconds.to_i + end + + before do + controller.idv_session.async_result_started_at = expired_started_at + end + + it 'displays an error' do + get :show + + expect(response).to render_template :new + expect(flash[:warning]).to include(t('idv.modal.financials.timeout')) + end + + it 'tracks the failure as a timeout' do + stub_analytics + allow(@analytics).to receive(:track_event) + + get :show + + result = { + success: false, + errors: { timed_out: ['Timed out waiting for vendor response'] }, + } + + expect(@analytics).to have_received(:track_event).with( + Analytics::IDV_PHONE_CONFIRMATION_VENDOR, result + ) + end + end + + context 'when the background job has completed' do + let(:result_id) { SecureRandom.uuid } + + before do + controller.idv_session.async_result_id = result_id + VendorValidatorResultStorage.new.store(result_id: result_id, result: result) + end + + let(:result) { Idv::VendorResult.new(success: true) } + + context 'when the phone is invalid' do + let(:result) do + Idv::VendorResult.new( + success: false, + errors: { phone: ['The phone number could not be verified.'] } + ) + end + + let(:params) { { phone: bad_phone } } + let(:user) { build(:user, phone: bad_phone, phone_confirmed_at: Time.zone.now) } + + it 'tracks event with invalid phone' do + stub_analytics + allow(@analytics).to receive(:track_event) + + get :show + + result = { + success: false, + errors: { + phone: ['The phone number could not be verified.'], + }, + } + + expect(flash[:warning]).to match t('idv.modal.phone.heading') + expect(flash[:warning]).to match t('idv.modal.attempts', count: max_attempts - 1) + expect(@analytics).to have_received(:track_event).with( + Analytics::IDV_PHONE_CONFIRMATION_VENDOR, result + ) + end + end context 'attempt window has expired, previous attempts == max-1' do let(:two_days_ago) { Time.zone.now - 2.days } let(:user) { build(:user, phone: good_phone, phone_confirmed_at: Time.zone.now) } before do - stub_verify_steps_one_and_two(user) user.idv_attempts = max_attempts - 1 user.idv_attempted_at = two_days_ago end it 'allows and does not affect attempt counter' do - put :create, idv_phone_form: { phone: good_phone } + get :show expect(response).to redirect_to verify_review_path expect(user.idv_attempts).to eq(max_attempts - 1) expect(user.idv_attempted_at).to eq two_days_ago end end + + it 'passes the normalized phone to the background job' do + user = build(:user, phone: good_phone, phone_confirmed_at: Time.zone.now) + stub_verify_steps_one_and_two(user) + + expect(SubmitIdvJob).to receive(:new).with( + vendor_validator_class: Idv::PhoneValidator, + idv_session: subject.idv_session, + vendor_params: normalized_phone + ).and_call_original + + put :create, idv_phone_form: { phone: good_phone } + end end end end diff --git a/spec/controllers/verify/review_controller_spec.rb b/spec/controllers/verify/review_controller_spec.rb index ff31a0be5f2..1c8a2e7731e 100644 --- a/spec/controllers/verify/review_controller_spec.rb +++ b/spec/controllers/verify/review_controller_spec.rb @@ -36,7 +36,7 @@ issuer: nil ) idv_session.profile_confirmation = true - idv_session.phone_confirmation = true + idv_session.vendor_phone_confirmation = true idv_session.financials_confirmation = true idv_session.params = user_attrs idv_session.normalized_applicant_params = user_attrs.merge( @@ -77,7 +77,7 @@ def show context 'user has missed address step' do before do - idv_session.phone_confirmation = false + idv_session.vendor_phone_confirmation = false end it 'redirects to address step' do diff --git a/spec/controllers/verify/sessions_controller_spec.rb b/spec/controllers/verify/sessions_controller_spec.rb index b0e90722b48..60c93db8a4d 100644 --- a/spec/controllers/verify/sessions_controller_spec.rb +++ b/spec/controllers/verify/sessions_controller_spec.rb @@ -28,6 +28,7 @@ let(:idv_session) do Idv::Session.new(user_session: subject.user_session, current_user: user, issuer: nil) end + let(:normalized_applicant) { Proofer::Applicant.new(user_attrs) } describe 'before_actions' do it 'includes before_actions from AccountStateChecker' do @@ -104,13 +105,11 @@ result = { success: false, - idv_attempts_exceeded: false, errors: { ssn: [t('idv.errors.duplicate_ssn')] }, - vendor: { reasons: nil }, } expect(@analytics).to receive(:track_event). - with(Analytics::IDV_BASIC_INFO_SUBMITTED, result) + with(Analytics::IDV_BASIC_INFO_SUBMITTED_FORM, result) post :create, profile: user_attrs.merge(ssn: '666-66-1234') @@ -129,139 +128,259 @@ end context 'missing fields' do - it 'checks for required fields' do - partial_attrs = user_attrs.dup - partial_attrs.delete :first_name + let(:partial_attrs) do + user_attrs.tap { |attrs| attrs.delete :first_name } + end + it 'checks for required fields' do post :create, profile: partial_attrs expect(response).to render_template(:new) expect(flash[:warning]).to be_nil end + + it 'does not increment attempts count' do + expect { post :create, profile: partial_attrs }. + to_not change(user, :idv_attempts) + end end + end - context 'un-resolvable attributes' do - let(:bad_attrs) { user_attrs.dup.merge(first_name: 'Bad') } + describe '#show' do + before do + stub_analytics + allow(@analytics).to receive(:track_event) + end - it 're-renders form' do - post :create, profile: bad_attrs + context 'when the background job is not complete yet' do + render_views - expect(flash[:warning]).to match t('idv.modal.sessions.heading') - expect(flash[:warning]).to match(t('idv.modal.attempts', count: max_attempts - 1)) - expect(response).to render_template(:new) - end + it 'renders a spinner and has the page refresh' do + get :show - it 'creates analytics event' do - post :create, profile: bad_attrs + expect(response).to render_template('shared/refresh') - result = { - success: false, - idv_attempts_exceeded: false, - errors: { - first_name: ['Unverified first name.'], - }, - vendor: { reasons: ['The name was suspicious'] }, - } - - expect(@analytics).to have_received(:track_event). - with(Analytics::IDV_BASIC_INFO_SUBMITTED, result) + dom = Nokogiri::HTML(response.body) + expect(dom.css('meta[http-equiv="refresh"]')).to be_present end end - context 'vendor agent throws exception' do - let(:bad_attrs) { user_attrs.dup.merge(first_name: 'Fail') } + context 'when the background job has timed out' do + let(:expired_started_at) do + Time.zone.now.to_i - Figaro.env.async_job_refresh_max_wait_seconds.to_i + end + + before do + controller.idv_session.async_result_started_at = expired_started_at + controller.idv_session.params = user_attrs + end - it 'logs failure and re-renders form' do - exception_msg = 'Failed to contact proofing vendor' + it 'displays an error' do + get :show - expect(NewRelic::Agent).to receive(:notice_error). - with(kind_of(StandardError)) + expect(response).to render_template :new + expect(flash[:warning]).to include(t('idv.modal.sessions.timeout')) + end - post :create, profile: bad_attrs + it 'tracks the failure as a timeout' do + stub_analytics + allow(@analytics).to receive(:track_event) + + get :show result = { success: false, + errors: { timed_out: ['Timed out waiting for vendor response'] }, idv_attempts_exceeded: false, - errors: { - agent: [exception_msg], - }, - vendor: { reasons: [exception_msg] }, + vendor: { reasons: [] }, } - expect(@analytics).to have_received(:track_event). - with(Analytics::IDV_BASIC_INFO_SUBMITTED, result) - expect(response).to render_template(:new) + expect(@analytics).to have_received(:track_event).with( + Analytics::IDV_BASIC_INFO_SUBMITTED_VENDOR, result + ) end end - context 'success' do - it 'creates analytics event' do - post :create, profile: user_attrs + context 'when the background job has completed' do + let(:result_id) { SecureRandom.uuid } + let(:params) { user_attrs } - result = { - success: true, - idv_attempts_exceeded: false, - errors: {}, - vendor: { reasons: ['Everything looks good'] }, - } + before do + controller.idv_session.async_result_id = result_id + VendorValidatorResultStorage.new.store(result_id: result_id, result: result) - expect(@analytics).to have_received(:track_event). - with(Analytics::IDV_BASIC_INFO_SUBMITTED, result) + controller.idv_session.params = params end - end - context 'max attempts exceeded' do - before do - user.idv_attempts = max_attempts - user.idv_attempted_at = Time.zone.now + context 'un-resolvable attributes' do + let(:params) { user_attrs.dup.merge(first_name: 'Bad') } + + let(:result) do + Idv::VendorResult.new( + success: false, + errors: { first_name: ['Unverified first name.'] }, + reasons: ['The name was suspicious'] + ) + end + + it 're-renders form' do + get :show + + expect(flash[:warning]).to match t('idv.modal.sessions.heading') + expect(flash[:warning]).to match(t('idv.modal.attempts', count: max_attempts - 1)) + expect(response).to render_template(:new) + end + + it 'creates analytics event' do + get :show + + result = { + success: false, + idv_attempts_exceeded: false, + errors: { + first_name: ['Unverified first name.'], + }, + vendor: { reasons: ['The name was suspicious'] }, + } + + expect(@analytics).to have_received(:track_event). + with(Analytics::IDV_BASIC_INFO_SUBMITTED_VENDOR, result) + end end - it 'redirects to fail' do - post :create, profile: user_attrs + context 'previous address supplied' do + let(:bad_zipcode) { '00000' } - result = { - request_path: verify_session_path, - } + let(:result) { Idv::VendorResult.new(success: false) } - expect(@analytics).to have_received(:track_event). - with(Analytics::IDV_MAX_ATTEMPTS_EXCEEDED, result) - expect(response).to redirect_to verify_fail_url - end - end + context 'if previous address has a bad zipcode' do + let(:params) { user_attrs.merge(previous_address).merge(prev_zipcode: bad_zipcode) } - context 'attempt window has expired, previous attempts == max-1' do - before do - user.idv_attempts = max_attempts - 1 - user.idv_attempted_at = Time.zone.now - 2.days + it 'fails' do + get :show + + expect(idv_session.resolution_successful).to be_nil + end + end + + context 'if current address has a bad zipcode' do + let(:params) { user_attrs.merge(previous_address).merge(zipcode: bad_zipcode) } + + it 'fails' do + get :show + + expect(idv_session.resolution_successful).to be_nil + end + end + + context 'with multiple addresses' do + let(:result) do + Idv::VendorResult.new(success: true, normalized_applicant: normalized_applicant) + end + let(:params) { user_attrs.merge(previous_address) } + + it 'respects both addresses' do + get :show + + expect(idv_session.resolution_successful).to eq true + end + end end - it 'allows and resets attempt counter' do - post :create, profile: user_attrs + context 'vendor agent throws exception' do + let(:params) { user_attrs.dup.merge(first_name: 'Fail') } + let(:exception_msg) { 'Failed to contact proofing vendor' } + let(:result) do + Idv::VendorResult.new( + success: false, + errors: { agent: [exception_msg] }, + reasons: [exception_msg] + ) + end + + it 'logs failure and re-renders form' do + get :show + + result = { + success: false, + idv_attempts_exceeded: false, + errors: { + agent: [exception_msg], + }, + vendor: { reasons: [exception_msg] }, + } + + expect(@analytics).to have_received(:track_event). + with(Analytics::IDV_BASIC_INFO_SUBMITTED_VENDOR, result) + expect(response).to render_template(:new) + end + end - expect(response).to redirect_to verify_finance_path - expect(user.idv_attempts).to eq 1 + context 'success' do + let(:result) do + Idv::VendorResult.new( + success: true, + reasons: ['Everything looks good'], + normalized_applicant: normalized_applicant + ) + end + + it 'creates analytics event' do + get :show + + result = { + success: true, + idv_attempts_exceeded: false, + errors: {}, + vendor: { reasons: ['Everything looks good'] }, + } + + expect(@analytics).to have_received(:track_event). + with(Analytics::IDV_BASIC_INFO_SUBMITTED_VENDOR, result) + end + + it 'increments attempts count' do + expect { get :show }.to change(user, :idv_attempts).by(1) + end end - end - context 'previous address supplied' do - let(:bad_zipcode) { '00000' } + context 'max attempts exceeded' do + let(:result) { Idv::VendorResult.new(success: true) } - it 'fails if previous address has bad zipcode' do - post :create, profile: user_attrs.merge(previous_address).merge(prev_zipcode: bad_zipcode) + before do + user.idv_attempts = max_attempts + user.idv_attempted_at = Time.zone.now + end - expect(idv_session.resolution_successful).to be_nil - end + it 'redirects to fail' do + get :show - it 'fails if current address has bad zipcode' do - post :create, profile: user_attrs.merge(previous_address).merge(zipcode: bad_zipcode) + result = { + request_path: verify_session_result_path, + } - expect(idv_session.resolution_successful).to be_nil + expect(@analytics).to have_received(:track_event). + with(Analytics::IDV_MAX_ATTEMPTS_EXCEEDED, result) + expect(response).to redirect_to verify_fail_url + end end - it 'respects both addresses' do - post :create, profile: user_attrs.merge(previous_address) + context 'attempt window has expired, previous attempts == max-1' do + let(:result) do + Idv::VendorResult.new(success: true, normalized_applicant: normalized_applicant) + end + + before do + user.idv_attempts = max_attempts - 1 + user.idv_attempted_at = Time.zone.now - 2.days + end + + it 'allows and resets attempt counter' do + get :show - expect(idv_session.resolution_successful).to eq true + expect(response).to redirect_to verify_finance_path + expect(user.idv_attempts).to eq 1 + end end end end diff --git a/spec/decorators/usps_decorator_spec.rb b/spec/decorators/usps_decorator_spec.rb index 890182ba499..d26a29f2aaf 100644 --- a/spec/decorators/usps_decorator_spec.rb +++ b/spec/decorators/usps_decorator_spec.rb @@ -1,23 +1,16 @@ require 'rails_helper' RSpec.describe UspsDecorator do + let(:user) { create(:user) } subject(:decorator) do - user = create( - :user, - :signed_up, - profiles: [build(:profile, :active, :verified, pii: { first_name: 'Jane' })] - ) - - idv_session = Idv::Session.new(user_session: {}, current_user: user, issuer: nil) - UspsDecorator.new(idv_session) + usps_mail_service = Idv::UspsMail.new(user) + UspsDecorator.new(usps_mail_service) end describe '#title' do context 'a letter has not been sent' do - let(:idv_session) { subject.idv_session } - it 'provides text to send' do - subject.idv_session.address_verification_mechanism = nil + allow(subject.usps_mail_service).to receive(:any_mail_sent?).and_return(false) expect(subject.title).to eq( I18n.t('idv.titles.mail.verify') ) @@ -26,7 +19,7 @@ context 'a letter has been sent' do it 'provides text to resend' do - subject.idv_session.address_verification_mechanism = 'usps' + allow(subject.usps_mail_service).to receive(:any_mail_sent?).and_return(true) expect(subject.title).to eq( I18n.t('idv.titles.mail.resend') ) @@ -37,7 +30,7 @@ describe '#button' do context 'a letter has not been sent' do it 'provides text to send' do - subject.idv_session.address_verification_mechanism = nil + allow(subject.usps_mail_service).to receive(:any_mail_sent?).and_return(false) expect(subject.button).to eq( I18n.t('idv.buttons.mail.send') ) @@ -46,7 +39,7 @@ context 'a letter has been sent' do it 'provides text to resend' do - subject.idv_session.address_verification_mechanism = 'usps' + allow(subject.usps_mail_service).to receive(:any_mail_sent?).and_return(true) expect(subject.button).to eq( I18n.t('idv.buttons.mail.resend') ) diff --git a/spec/factories/service_providers.rb b/spec/factories/service_providers.rb index 58de449c4cc..434703fbc71 100644 --- a/spec/factories/service_providers.rb +++ b/spec/factories/service_providers.rb @@ -1,5 +1,5 @@ FactoryGirl.define do - Faker::Config.locale = 'en-US' + Faker::Config.locale = :en factory :service_provider do cert { 'saml_test_sp' } diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 8781769339c..fac8cf7ddb1 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -1,5 +1,5 @@ FactoryGirl.define do - Faker::Config.locale = 'en-US' + Faker::Config.locale = :en factory :user do confirmed_at Time.zone.now diff --git a/spec/features/accessibility/idv_pages_spec.rb b/spec/features/accessibility/idv_pages_spec.rb index 95c9f14b8a2..290b9c68025 100644 --- a/spec/features/accessibility/idv_pages_spec.rb +++ b/spec/features/accessibility/idv_pages_spec.rb @@ -1,7 +1,7 @@ require 'rails_helper' require 'axe/rspec' -feature 'Accessibility on IDV pages', :js do +feature 'Accessibility on IDV pages', :js, idv_job: true do describe 'IDV pages' do include IdvHelper diff --git a/spec/features/flows/sp_authentication_flows_spec.rb b/spec/features/flows/sp_authentication_flows_spec.rb index 49fdc1dd6f8..b99eda80d5f 100644 --- a/spec/features/flows/sp_authentication_flows_spec.rb +++ b/spec/features/flows/sp_authentication_flows_spec.rb @@ -1,224 +1,230 @@ require 'rails_helper' -include IdvHelper -include SamlAuthHelper feature 'SP-initiated authentication with login.gov', user_flow: true do - context 'with a valid SP' do - context 'when LOA3' do - before do - visit loa3_authnrequest - end - - it 'prompts the user to create an account or sign in' do - screenshot_and_save_page - end - - context 'when choosing Create Account' do - before do - click_link t('sign_up.registrations.create_account') - end - - it 'prompts for email address' do - screenshot_and_save_page - end + include IdvHelper + include SamlAuthHelper - context 'with a valid email address submitted' do + I18n.available_locales.each do |locale| + context "with locale=#{locale}" do + context 'with a valid SP' do + context 'when LOA3' do before do - @email = Faker::Internet.safe_email - fill_in 'Email', with: @email - click_button t('forms.buttons.submit.default') - @user = User.find_with_email(@email) + visit "#{loa3_authnrequest}&locale=#{locale}" end - it 'informs the user to check email' do + it 'prompts the user to create an account or sign in' do screenshot_and_save_page end - context 'with a confirmed email address' do + context 'when choosing Create Account' do before do - confirm_last_user + click_link t('sign_up.registrations.create_account') end - it 'prompts the user for a password' do + it 'prompts for email address' do screenshot_and_save_page end - context 'with a valid password' do + context 'with a valid email address submitted' do before do - fill_in 'password_form_password', with: Features::SessionHelper::VALID_PASSWORD - click_button t('forms.buttons.continue') + @email = Faker::Internet.safe_email + fill_in 'user_email', with: @email + click_button t('forms.buttons.submit.default') + @user = User.find_with_email(@email) end - it 'prompts the user to configure 2FA' do + it 'informs the user to check email' do screenshot_and_save_page end - context 'with a valid phone number' do + context 'with a confirmed email address' do before do - fill_in 'Phone', with: Faker::PhoneNumber.cell_phone + confirm_last_user end - context 'with SMS delivery' do + it 'prompts the user for a password' do + screenshot_and_save_page + end + + context 'with a valid password' do before do - choose t('devise.two_factor_authentication.otp_delivery_preference.sms') - click_send_security_code + fill_in 'password_form_password', with: Features::SessionHelper::VALID_PASSWORD + click_button t('forms.buttons.continue') end - it 'prompts for OTP' do + it 'prompts the user to configure 2FA' do screenshot_and_save_page end - context 'with valid OTP confirmation' do + context 'with a valid phone number' do before do - fill_in 'code', with: @user.reload.direct_otp - click_button t('forms.buttons.submit.default') - end - - it 'prompts the user to verify oneself' do - screenshot_and_save_page + fill_in 'two_factor_setup_form_phone', with: Faker::PhoneNumber.cell_phone end - context 'when choosing Yes, continue' do + context 'with SMS delivery' do before do - click_link t('idv.index.continue_link') + choose t('devise.two_factor_authentication.otp_delivery_preference.sms') + click_send_security_code end - it 'prompts for personal information' do + it 'prompts for OTP' do screenshot_and_save_page end - context 'with valid personal information entered' do + context 'with valid OTP confirmation' do before do - fill_in t('idv.form.first_name'), with: Faker::Name.first_name - fill_in t('idv.form.last_name'), with: Faker::Name.last_name - fill_in 'profile_address1', with: '123 Main St' - fill_in 'profile_city', with: Faker::Address.city - find('#profile_state').find(:xpath, - "option[#{(1..50).to_a.sample}]"). - select_option - fill_in 'profile_zipcode', with: Faker::Address.zip_code - fill_in t('idv.form.dob'), with: "09/09/#{(1900..2000).to_a.sample}" - fill_in 'profile_ssn', with: "999-99-#{(1000..9999).to_a.sample}" - click_button t('forms.buttons.continue') + fill_in 'code', with: @user.reload.direct_otp + click_button t('forms.buttons.submit.default') end - it 'prompts for the last 8 digits of a credit card' do + it 'prompts the user to verify oneself' do screenshot_and_save_page end - context 'with last 8 digits of credit card' do + context 'when choosing Yes, continue' do before do - fill_out_financial_form_ok + click_link t('idv.index.continue_link') end - it 'prompts to activate account by phone or mail' do + it 'prompts for personal information' do screenshot_and_save_page end - end - context 'without a credit card' do - before do - click_link t('idv.form.use_financial_account') - end - - it 'prompts user to provide a financial account number' do - screenshot_and_save_page - end - - context 'with a valid financial account' do + context 'with valid personal information entered' do before do - select t('idv.form.mortgage'), from: 'idv_finance_form_finance_type' - fill_in 'idv_finance_form_mortgage', with: '12345678' - # click_idv_continue doesn't work with the JavaScript on this page - # and enabling js: true causes unexpected behavior - form = page.find('#new_idv_finance_form') - class << form - def submit! - Capybara::RackTest::Form.new(driver, native).submit({}) - end - end - form.submit! + fill_in 'profile_first_name', with: Faker::Name.first_name + fill_in 'profile_last_name', with: Faker::Name.last_name + fill_in 'profile_address1', with: '123 Main St' + fill_in 'profile_city', with: Faker::Address.city + find('#profile_state'). + find(:xpath, "option[#{(1..50).to_a.sample}]"). + select_option + fill_in 'profile_zipcode', with: Faker::Address.zip_code + fill_in 'profile_dob', with: "09/09/#{(1900..2000).to_a.sample}" + fill_in 'profile_ssn', with: "999-99-#{(1000..9999).to_a.sample}" + click_button t('forms.buttons.continue') end - it 'prompts to activate account by phone or mail' do + it 'prompts for the last 8 digits of a credit card' do screenshot_and_save_page end - context 'when activating by phone' do + context 'with last 8 digits of credit card' do before do - click_idv_address_choose_phone + fill_out_financial_form_ok end - it 'prompts the user to confirm or enter phone number' do + it 'prompts to activate account by phone or mail' do screenshot_and_save_page end end - context 'when activating by mail' do + context 'without a credit card' do before do - click_idv_address_choose_usps + click_link t('idv.form.use_financial_account') end - it 'prompts the user to confirm' do + it 'prompts user to provide a financial account number' do screenshot_and_save_page end - context 'when confirming to mail' do + context 'with a valid financial account' do before do - click_on t('idv.buttons.mail.send') + select t('idv.form.mortgage'), + from: 'idv_finance_form_finance_type' + fill_in 'idv_finance_form_mortgage', with: '12345678' + # click_idv_continue doesn't work with the JavaScript on this page + # and enabling js: true causes unexpected behavior + form = page.find('#new_idv_finance_form') + class << form + def submit! + Capybara::RackTest::Form.new(driver, native).submit({}) + end + end + form.submit! end - it 'prompts user for password to encrypt profile' do + it 'prompts to activate account by phone or mail' do screenshot_and_save_page end - context 'when confirming password' do + context 'when activating by phone' do + before do + click_idv_address_choose_phone + end + + it 'prompts the user to confirm or enter phone number' do + screenshot_and_save_page + end + end + + context 'when activating by mail' do before do - fill_in 'user_password', - with: Features::SessionHelper::VALID_PASSWORD - click_button t('forms.buttons.submit.default') + click_idv_address_choose_usps end - it 'provides a new personal key and prompts for verification' do + it 'prompts the user to confirm' do screenshot_and_save_page end - context 'when clicking Continue' do + context 'when confirming to mail' do before do - click_acknowledge_personal_key + click_on t('idv.buttons.mail.send') end - it 'displays the user profile' do + it 'prompts user for password to encrypt profile' do screenshot_and_save_page end + + context 'when confirming password' do + before do + fill_in 'user_password', + with: Features::SessionHelper::VALID_PASSWORD + click_button t('forms.buttons.submit.default') + end + + it 'provides a new personal key and prompts to verify' do + screenshot_and_save_page + end + + context 'when clicking Continue' do + before do + click_acknowledge_personal_key + end + + it 'displays the user profile' do + screenshot_and_save_page + end + end + end end end + + # Disabling this spec because of js: true issue + # Will re-enable this once resolved + # context 'when choosing to cancel' do + # before do + # click_button t('links.cancel_idv') + # end + + # it 'prompts to continue verification or visit profile' do + # screenshot_and_save_page + # end + # end end end - - # Disabling this spec because of js: true issue - # Will re-enable this once resolved - # context 'when choosing to cancel' do - # before do - # click_button t('links.cancel_idv') - # end - - # it 'prompts to continue verification or visit profile' do - # screenshot_and_save_page - # end - # end end - end - end - context 'with invalid personal information entered' do - before do - fill_out_idv_form_fail - click_button t('forms.buttons.continue') - end + context 'with invalid personal information entered' do + before do + fill_out_idv_form_fail + click_button t('forms.buttons.continue') + end - it 'presents a modal with current retries remaining' do - screenshot_and_save_page + it 'presents a modal with current retries remaining' do + screenshot_and_save_page + end + end end end end @@ -227,147 +233,147 @@ def submit! end end end - end - end - - # context 'when choosing to sign in' do - # TODO: duplicate scenarios from Create Account here - # end - end - - context 'when LOA1' do - before do - visit authnrequest_get - end - - it 'prompts the user to create an account or sign in' do - screenshot_and_save_page - end - - context 'when choosing Create Account' do - before do - click_link t('sign_up.registrations.create_account') - end - it 'prompts for email address' do - screenshot_and_save_page + # context 'when choosing to sign in' do + # TODO: duplicate scenarios from Create Account here + # end end - context 'with a valid email address submitted' do + context 'when LOA1' do before do - @email = Faker::Internet.safe_email - fill_in 'Email', with: @email - click_button t('forms.buttons.submit.default') - @user = User.find_with_email(@email) + visit "#{authnrequest_get}&locale=#{locale}" end - it 'informs the user to check email' do + it 'prompts the user to create an account or sign in' do screenshot_and_save_page end - context 'with a confirmed email address' do + context 'when choosing Create Account' do before do - confirm_last_user + click_link t('sign_up.registrations.create_account') end - it 'prompts the user for a password' do + it 'prompts for email address' do screenshot_and_save_page end - context 'with a valid password' do + context 'with a valid email address submitted' do before do - fill_in 'password_form_password', with: Features::SessionHelper::VALID_PASSWORD - click_button t('forms.buttons.continue') + @email = Faker::Internet.safe_email + fill_in 'user_email', with: @email + click_button t('forms.buttons.submit.default') + @user = User.find_with_email(@email) end - it 'prompts the user to configure 2FA' do + it 'informs the user to check email' do screenshot_and_save_page end - context 'with a valid phone number' do + context 'with a confirmed email address' do before do - fill_in 'Phone', with: Faker::PhoneNumber.cell_phone + confirm_last_user end - context 'with SMS delivery' do + it 'prompts the user for a password' do + screenshot_and_save_page + end + + context 'with a valid password' do before do - choose t('devise.two_factor_authentication.otp_delivery_preference.sms') - click_send_security_code + fill_in 'password_form_password', with: Features::SessionHelper::VALID_PASSWORD + click_button t('forms.buttons.continue') end - it 'prompts for OTP' do + it 'prompts the user to configure 2FA' do screenshot_and_save_page end - end - context 'with Voice delivery' do - before do - choose t('devise.two_factor_authentication.otp_delivery_preference.voice') - click_send_security_code - end + context 'with a valid phone number' do + before do + fill_in 'two_factor_setup_form_phone', with: Faker::PhoneNumber.cell_phone + end - it 'prompts for OTP' do - screenshot_and_save_page + context 'with SMS delivery' do + before do + choose t('devise.two_factor_authentication.otp_delivery_preference.sms') + click_send_security_code + end + + it 'prompts for OTP' do + screenshot_and_save_page + end + end + + context 'with Voice delivery' do + before do + choose t('devise.two_factor_authentication.otp_delivery_preference.voice') + click_send_security_code + end + + it 'prompts for OTP' do + screenshot_and_save_page + end + end end end end end end - end - end - - context 'when choosing to sign in' do - before do - @user = create(:user, :signed_up) - click_link t('links.sign_in') - end - - context 'with valid credentials entered' do - before do - fill_in_credentials_and_submit(@user.email, @user.password) - end - it 'prompts for 2FA delivery method' do - screenshot_and_save_page - end - - context 'with SMS OTP selected (default)' do - it 'prompts for OTP verification' do - screenshot_and_save_page + context 'when choosing to sign in' do + before do + @user = create(:user, :signed_up) + click_link t('links.sign_in') end - context 'with valid OTP confirmation' do + context 'with valid credentials entered' do before do - fill_in 'code', with: @user.reload.direct_otp - click_button t('forms.buttons.submit.default') + fill_in_credentials_and_submit(@user.email, @user.password) end - # Skipping since we have nothing to show: this occurs on the SP - xit 'redirects back to SP' do + it 'prompts for 2FA delivery method' do screenshot_and_save_page end - end - end - end - context 'without a valid username and password' do - context 'when choosing "Forgot your password?"' do - before do - click_link t('links.passwords.forgot') - end + context 'with SMS OTP selected (default)' do + it 'prompts for OTP verification' do + screenshot_and_save_page + end - it 'prompts for my email address' do - screenshot_and_save_page - end + context 'with valid OTP confirmation' do + before do + fill_in 'code', with: @user.reload.direct_otp + click_button t('forms.buttons.submit.default') + end - context 'with not_a_real_email_dot.com submitted' do - before do - fill_in 'password_reset_email_form_email', with: 'not_a_real_email_dot.com' - click_button t('forms.buttons.continue') + # Skipping since we have nothing to show: this occurs on the SP + xit 'redirects back to SP' do + screenshot_and_save_page + end + end end + end - it 'displays a useful error' do - screenshot_and_save_page + context 'without a valid username and password' do + context 'when choosing "Forgot your password?"' do + before do + click_link t('links.passwords.forgot') + end + + it 'prompts for my email address' do + screenshot_and_save_page + end + + context 'with not_a_real_email_dot.com submitted' do + before do + fill_in 'password_reset_email_form_email', with: 'not_a_real_email_dot.com' + click_button t('forms.buttons.continue') + end + + it 'displays a useful error' do + screenshot_and_save_page + end + end end end end diff --git a/spec/features/flows/visitor_flows_spec.rb b/spec/features/flows/visitor_flows_spec.rb index 089cd9c0f61..7bae6f6f0d6 100644 --- a/spec/features/flows/visitor_flows_spec.rb +++ b/spec/features/flows/visitor_flows_spec.rb @@ -1,187 +1,191 @@ require 'rails_helper' feature 'Visitors requesting login.gov directly', devise: true, user_flow: true do - context 'when visiting the homepage' do - before do - visit root_path - end - - it 'loads the home page' do - screenshot_and_save_page - end - - describe 'showing the password' do - it 'allows me to see my password', js: true do - visit new_user_session_path - fill_in 'Password', with: 'my password' - screenshot_and_save_page - end - end - - context 'when attempting to sign in' do - context 'with a valid account' do + I18n.available_locales.each do |locale| + context "with locale=#{locale}" do + context 'when visiting the homepage' do before do - @user = create(:user, :signed_up) - fill_in 'Email', with: @user.email - fill_in 'Password', with: @user.password - page.find('.btn-primary').click + visit root_path(locale: locale) end - it 'sends OTP via previously chosen method' do + it 'loads the home page' do screenshot_and_save_page end - context 'with a valid OTP' do - before do - fill_in 'code', with: @user.reload.direct_otp - click_button t('forms.buttons.submit.default') - end - - it 'redirects to profile' do + describe 'showing the password' do + it 'allows me to see my password', js: true do + visit new_user_session_path(locale: locale) + fill_in 'user_password', with: 'my password' screenshot_and_save_page end end - context 'with an invalid OTP submitted' do - before do - fill_in 'code', with: '123abc' - click_button t('forms.buttons.submit.default') - end - - it 'displays a useful error' do - screenshot_and_save_page - end - - context 'with a second invalid OTP submitted' do + context 'when attempting to sign in' do + context 'with a valid account' do before do - fill_in 'code', with: '123abc' - click_button t('forms.buttons.submit.default') + @user = create(:user, :signed_up) + fill_in 'user_email', with: @user.email + fill_in 'user_password', with: @user.password + page.find('.btn-primary').click end - it 'displays a useful error' do + it 'sends OTP via previously chosen method' do screenshot_and_save_page end - context 'with a third invalid OTP submitted' do + context 'with a valid OTP' do + before do + fill_in 'code', with: @user.reload.direct_otp + click_button t('forms.buttons.submit.default') + end + + it 'redirects to profile' do + screenshot_and_save_page + end + end + + context 'with an invalid OTP submitted' do before do fill_in 'code', with: '123abc' click_button t('forms.buttons.submit.default') end - it 'displays a useful error and locks the user account' do + it 'displays a useful error' do screenshot_and_save_page end + + context 'with a second invalid OTP submitted' do + before do + fill_in 'code', with: '123abc' + click_button t('forms.buttons.submit.default') + end + + it 'displays a useful error' do + screenshot_and_save_page + end + + context 'with a third invalid OTP submitted' do + before do + fill_in 'code', with: '123abc' + click_button t('forms.buttons.submit.default') + end + + it 'displays a useful error and locks the user account' do + screenshot_and_save_page + end + end + end end end - end - end - context 'without valid credentials' do - before do - fill_in 'Email', with: Faker::Internet.safe_email - fill_in 'Password', with: 'my password' - page.find('.btn-primary').click - end + context 'without valid credentials' do + before do + fill_in 'user_email', with: Faker::Internet.safe_email + fill_in 'user_password', with: 'my password' + page.find('.btn-primary').click + end - it 'displays a useful error message' do - screenshot_and_save_page + it 'displays a useful error message' do + screenshot_and_save_page + end + end end - end - end - context 'when choosing create account' do - before do - click_link t('links.create_account') - end + context 'when choosing create account' do + before do + click_link t('links.create_account') + end - it 'informs the user about login.gov' do - screenshot_and_save_page - end + it 'informs the user about login.gov' do + screenshot_and_save_page + end - context 'when creating account with valid email' do - before do - sign_up_with(Faker::Internet.safe_email) - end + context 'when creating account with valid email' do + before do + sign_up_with(Faker::Internet.safe_email) + end - it 'notifies user to check email' do - screenshot_and_save_page - end + it 'notifies user to check email' do + screenshot_and_save_page + end - context 'when confirming email' do - before do - confirm_last_user + context 'when confirming email' do + before do + confirm_last_user + end + + it 'prompts user to set password' do + screenshot_and_save_page + end + end end - it 'prompts user to set password' do - screenshot_and_save_page + context 'when attempting with an invalid email' do + before do + sign_up_with('kevin@kevin') + end + + it 'informs the user to try again' do + screenshot_and_save_page + end end end end - context 'when attempting with an invalid email' do + context 'when choosing \'Forgot your password?' do before do - sign_up_with('kevin@kevin') + visit new_user_password_path(locale: locale) end - it 'informs the user to try again' do + it 'prompts for email address' do screenshot_and_save_page end - end - end - end - context 'when choosing \'Forgot your password?' do - before do - visit new_user_password_path - end + context 'when submitting email for an existing account' do + before do + @user = create(:user, :signed_up) + fill_in 'password_reset_email_form_email', with: @user.email + click_button t('forms.buttons.continue') + end - it 'prompts for email address' do - screenshot_and_save_page - end + it 'informs the user to check their email' do + screenshot_and_save_page + end - context 'when submitting email for an existing account' do - before do - @user = create(:user, :signed_up) - fill_in 'Email', with: @user.email - click_button t('forms.buttons.continue') - end + context 'when following link in email', email: true do + before do + open_last_email + click_email_link_matching(/reset_password_token/) + end - it 'informs the user to check their email' do - screenshot_and_save_page - end + it 'prompts the user to enter a new password' do + screenshot_and_save_page + end - context 'when following link in email', email: true do - before do - open_last_email - click_email_link_matching(/reset_password_token/) - end + context 'when submitting a valid password' do + before do + fill_in t('forms.passwords.edit.labels.password'), with: 'NewVal!dPassw0rd' + click_button t('forms.passwords.edit.buttons.submit') + end - it 'prompts the user to enter a new password' do - screenshot_and_save_page + it 'redirects to the homepage with a helpful message' do + screenshot_and_save_page + end + end + end end - context 'when submitting a valid password' do + context 'when submitting email not associated with an account' do before do - fill_in t('forms.passwords.edit.labels.password'), with: 'NewVal!dPassw0rd' - click_button t('forms.passwords.edit.buttons.submit') + fill_in 'password_reset_email_form_email', with: 'non-existent-email@example.com' + click_button t('forms.buttons.continue') end - it 'redirects to the homepage with a helpful message' do + it 'informs the user to check their email' do screenshot_and_save_page end end end end - - context 'when submitting email not associated with an account' do - before do - fill_in 'Email', with: 'non-existent-email@example.com' - click_button t('forms.buttons.continue') - end - - it 'informs the user to check their email' do - screenshot_and_save_page - end - end end end diff --git a/spec/features/idv/flow_spec.rb b/spec/features/idv/flow_spec.rb index 4ebe49ed1dd..0b693cef18f 100644 --- a/spec/features/idv/flow_spec.rb +++ b/spec/features/idv/flow_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -feature 'IdV session' do +feature 'IdV session', idv_job: true do include IdvHelper context 'landing page' do @@ -65,7 +65,7 @@ fill_in 'profile_first_name', with: first_name_to_trigger_exception click_idv_continue - expect(current_path).to eq verify_session_path + expect(current_path).to eq verify_session_result_path expect(page).to have_css('.modal-warning', text: t('idv.modal.sessions.heading')) end @@ -76,7 +76,7 @@ visit verify_session_path complete_idv_profile_fail - expect(current_path).to eq verify_session_path + expect(current_path).to eq verify_session_result_path end user.reload @@ -109,7 +109,7 @@ fill_out_financial_form_fail click_idv_continue - expect(current_path).to eq verify_finance_path + expect(current_path).to eq verify_finance_result_path end fill_out_financial_form_fail @@ -152,7 +152,7 @@ click_idv_continue # failure reloads the form and shows warning modal - expect(current_path).to eq verify_session_path + expect(current_path).to eq verify_session_result_path expect(page).to have_css('.modal-warning', text: t('idv.modal.sessions.heading')) click_button t('idv.modal.button.warning') @@ -169,7 +169,7 @@ click_idv_continue # failure reloads the form and shows warning modal - expect(current_path).to eq verify_finance_path + expect(current_path).to eq verify_finance_result_path expect(page).to have_css('.modal-warning', text: t('idv.modal.financials.heading')) click_button t('idv.modal.button.warning') @@ -191,7 +191,7 @@ click_idv_continue # failure reloads the same sticky form (different path) and shows warning modal - expect(current_path).to eq verify_finance_path + expect(current_path).to eq verify_finance_result_path click_button t('idv.modal.button.warning') expect(page).to have_selector("input[value='#{mortgage_value}']") @@ -214,7 +214,7 @@ click_idv_continue # failure reloads the same sticky form - expect(current_path).to eq verify_phone_path + expect(current_path).to eq verify_phone_result_path expect(page).to have_css('.modal-warning', text: t('idv.modal.phone.heading')) click_button t('idv.modal.button.warning') expect(page).to have_selector("input[value='#{bad_phone_formatted}']") @@ -413,6 +413,29 @@ expect(current_path).to eq account_path end + + scenario 'being unable to verify account without OTP phone confirmation' do + different_phone = '555-555-9876' + user = sign_in_live_with_2fa + visit verify_session_path + + fill_out_idv_form_ok + click_idv_continue + fill_out_financial_form_ok + click_idv_continue + click_idv_address_choose_phone + fill_out_phone_form_ok(different_phone) + click_idv_continue + fill_in :user_password, with: user_password + click_submit_default + + visit verify_confirmations_path + click_acknowledge_personal_key + + user.reload + + expect(user.active_profile).to be_nil + end end def complete_idv_profile_fail diff --git a/spec/features/idv/interrupted_session_spec.rb b/spec/features/idv/interrupted_session_spec.rb index b326172ba1d..347e5891978 100644 --- a/spec/features/idv/interrupted_session_spec.rb +++ b/spec/features/idv/interrupted_session_spec.rb @@ -3,7 +3,7 @@ feature 'Interrupted IdV session' do include IdvHelper - describe 'Closing the browser while on the first form', js: true do + describe 'Closing the browser while on the first form', js: true, idv_job: true do before do sign_in_and_2fa_user visit verify_session_path diff --git a/spec/features/idv/phone_spec.rb b/spec/features/idv/phone_spec.rb index 2af68727238..2d824ef3117 100644 --- a/spec/features/idv/phone_spec.rb +++ b/spec/features/idv/phone_spec.rb @@ -3,7 +3,7 @@ feature 'Verify phone' do include IdvHelper - scenario 'phone step redirects to fail after max attempts' do + scenario 'phone step redirects to fail after max attempts', idv_job: true do sign_in_and_2fa_user visit verify_session_path fill_out_idv_form_ok @@ -16,7 +16,7 @@ fill_out_phone_form_fail click_idv_continue - expect(current_path).to eq verify_phone_path + expect(current_path).to eq verify_phone_result_path end fill_out_phone_form_fail @@ -24,7 +24,7 @@ expect(page).to have_css('.alert-error', text: t('idv.modal.phone.heading')) end - context 'Idv phone and user phone are different' do + context 'Idv phone and user phone are different', idv_job: true do scenario 'prompts to confirm phone' do user = create( :user, :signed_up, @@ -43,9 +43,31 @@ expect(current_path).to eq account_path end + + scenario 'phone number with no voice otp support only allows sms delivery' do + guam_phone = '671-555-5000' + user = create( + :user, :signed_up, + otp_delivery_preference: 'voice', + password: Features::SessionHelper::VALID_PASSWORD + ) + + sign_in_and_2fa_user(user) + visit verify_session_path + + allow(VoiceOtpSenderJob).to receive(:perform_later) + allow(SmsOtpSenderJob).to receive(:perform_later) + + complete_idv_profile_with_phone(guam_phone) + + expect(current_path).to eq login_two_factor_path(otp_delivery_preference: :sms) + expect(VoiceOtpSenderJob).to_not have_received(:perform_later) + expect(SmsOtpSenderJob).to have_received(:perform_later) + expect(page).to_not have_content(t('links.two_factor_authentication.resend_code.phone')) + end end - scenario 'phone field only allows numbers', js: true do + scenario 'phone field only allows numbers', js: true, idv_job: true do sign_in_and_2fa_user visit verify_session_path fill_out_idv_form_ok @@ -57,7 +79,7 @@ fill_in 'Phone', with: '' find('#idv_phone_form_phone').native.send_keys('abcd1234') - expect(find('#idv_phone_form_phone').value).to eq '1 (234) ' + expect(find('#idv_phone_form_phone').value).to eq '+1 234' end def complete_idv_profile_with_phone(phone) diff --git a/spec/features/idv/previous_address_spec.rb b/spec/features/idv/previous_address_spec.rb index 334cf48eaa3..71a018fd915 100644 --- a/spec/features/idv/previous_address_spec.rb +++ b/spec/features/idv/previous_address_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -feature 'IdV with previous address filled in' do +feature 'IdV with previous address filled in', idv_job: true do include IdvHelper let(:bad_zipcode) { '00000' } @@ -8,7 +8,7 @@ let(:previous_address) { '456 Other Ave' } def expect_to_stay_on_verify_session_page - expect(current_path).to eq verify_session_path + expect(current_path).to eq verify_session_result_path expect(page).to have_selector("input[value='#{bad_zipcode}']") end diff --git a/spec/features/openid_connect/openid_connect_spec.rb b/spec/features/openid_connect/openid_connect_spec.rb index b548a1ff952..ab2516c478b 100644 --- a/spec/features/openid_connect/openid_connect_spec.rb +++ b/spec/features/openid_connect/openid_connect_spec.rb @@ -111,7 +111,7 @@ click_submit_default expect(current_url).to start_with('http://localhost:7654/auth/result') - expect(page.get_rack_session.keys).to_not include('sp') + expect(page.get_rack_session.keys).to include('sp') end it 'auto-allows and sets the correct CSP headers after an incorrect OTP' do @@ -146,7 +146,7 @@ click_submit_default expect(current_url).to start_with('http://localhost:7654/auth/result') - expect(page.get_rack_session.keys).to_not include('sp') + expect(page.get_rack_session.keys).to include('sp') end end @@ -238,7 +238,7 @@ click_button t('forms.buttons.continue') redirect_uri = URI(current_url) expect(redirect_uri.to_s).to start_with('gov.gsa.openidconnect.test://result') - expect(page.get_rack_session.keys).to_not include('sp') + expect(page.get_rack_session.keys).to include('sp') end end end @@ -307,7 +307,7 @@ end context 'LOA3 signup' do - it 'redirects back to SP', email: true do + it 'redirects back to SP', email: true, idv_job: true do allow(FeatureManagement).to receive(:prefill_otp_codes?).and_return(true) client_id = 'urn:gov:gsa:openidconnect:sp:server' @@ -501,7 +501,7 @@ redirect_uri = URI(current_url) expect(redirect_uri.to_s).to start_with('http://localhost:7654/auth/result') - expect(page.get_rack_session.keys).to_not include('sp') + expect(page.get_rack_session.keys).to include('sp') end perform_in_browser(:one) do @@ -515,7 +515,7 @@ redirect_uri = URI(current_url) expect(redirect_uri.to_s).to start_with('http://localhost:7654/auth/result') - expect(page.get_rack_session.keys).to_not include('sp') + expect(page.get_rack_session.keys).to include('sp') end end end diff --git a/spec/features/saml/loa1_sso_spec.rb b/spec/features/saml/loa1_sso_spec.rb index dc974e046c7..669bb18703a 100644 --- a/spec/features/saml/loa1_sso_spec.rb +++ b/spec/features/saml/loa1_sso_spec.rb @@ -30,19 +30,22 @@ click_on t('forms.buttons.continue') expect(current_url).to eq authn_request - expect(page.get_rack_session.keys).to_not include('sp') + expect(page.get_rack_session.keys).to include('sp') end end it 'takes user to the service provider, allows user to visit IDP' do + allow(FeatureManagement).to receive(:prefill_otp_codes?).and_return(true) + user = create(:user, :signed_up) saml_authn_request = auth_request.create(saml_settings) visit saml_authn_request - sign_in_live_with_2fa(user) + click_link t('links.sign_in') + fill_in_credentials_and_submit(user.email, user.password) + click_submit_default expect(current_url).to eq saml_authn_request - expect(page.get_rack_session.keys).to_not include('sp') visit root_path expect(current_path).to eq account_path @@ -177,7 +180,7 @@ click_button t('forms.buttons.continue') expect(current_url).to eq authn_request - expect(page.get_rack_session.keys).to_not include('sp') + expect(page.get_rack_session.keys).to include('sp') end perform_in_browser(:one) do @@ -190,7 +193,7 @@ click_button t('forms.buttons.continue') expect(current_url).to eq authn_request - expect(page.get_rack_session.keys).to_not include('sp') + expect(page.get_rack_session.keys).to include('sp') end end end diff --git a/spec/features/saml/loa3_sso_spec.rb b/spec/features/saml/loa3_sso_spec.rb index 85785816185..f2014fb7225 100644 --- a/spec/features/saml/loa3_sso_spec.rb +++ b/spec/features/saml/loa3_sso_spec.rb @@ -1,9 +1,27 @@ require 'rails_helper' -feature 'LOA3 Single Sign On' do +feature 'LOA3 Single Sign On', idv_job: true do include SamlAuthHelper include IdvHelper + def perform_id_verification_with_usps_without_confirming_code_then_sign_out(user) + saml_authn_request = auth_request.create(loa3_with_bundle_saml_settings) + visit saml_authn_request + sign_in_live_with_2fa(user) + click_idv_begin + fill_out_idv_form_ok + click_idv_continue + fill_out_financial_form_ok + click_idv_continue + click_idv_address_choose_usps + click_on t('idv.buttons.mail.send') + fill_in :user_password, with: user.password + click_submit_default + click_acknowledge_personal_key + first(:link, t('links.sign_out')).click + click_submit_default + end + context 'First time registration' do let(:email) { 'test@test.com' } before do @@ -67,6 +85,7 @@ click_on t('idv.buttons.mail.send') expect(current_path).to eq verify_review_path + expect(page).to_not have_content t('idv.messages.phone.phone_of_record') fill_in :user_password, with: user_password @@ -186,6 +205,7 @@ sign_in_live_with_2fa(user) expect(current_path).to eq verify_account_path + expect(page).to have_content t('idv.messages.usps.resend') click_button t('forms.verify_profile.submit') @@ -196,6 +216,21 @@ expect(current_url).to eq saml_authn_request end + + it 'provides an option to send another letter' do + user = create(:user, :signed_up) + + perform_id_verification_with_usps_without_confirming_code_then_sign_out(user) + + sign_in_live_with_2fa(user) + + expect(current_path).to eq verify_account_path + + click_link(t('idv.messages.usps.resend')) + + expect(user.events.account_verified.size).to be(0) + expect(current_path).to eq(verify_usps_path) + end end context 'having previously cancelled phone verification' do @@ -213,6 +248,26 @@ expect(current_url).to eq saml_authn_request end end + + context 'returning to verify after canceling during the same session' do + it 'allows the user to verify' do + user = create(:user, :signed_up) + saml_authn_request = auth_request.create(loa3_with_bundle_saml_settings) + + visit saml_authn_request + sign_in_live_with_2fa(user) + click_idv_begin + fill_out_idv_form_ok + click_idv_continue + click_idv_cancel + visit saml_authn_request + click_idv_begin + fill_out_idv_form_ok + click_idv_continue + + expect(current_path).to eq verify_finance_path + end + end end context 'visiting sign_up_completed path before proofing' do diff --git a/spec/features/two_factor_authentication/change_factor_spec.rb b/spec/features/two_factor_authentication/change_factor_spec.rb index 49880e3891c..bfa396bcafb 100644 --- a/spec/features/two_factor_authentication/change_factor_spec.rb +++ b/spec/features/two_factor_authentication/change_factor_spec.rb @@ -54,6 +54,24 @@ expect(current_path).to eq account_path end + scenario 'editing phone number with no voice otp support only allows sms delivery' do + user.update(otp_delivery_preference: 'voice') + guam_phone = '671-555-5000' + + visit manage_phone_path + complete_2fa_confirmation + + allow(VoiceOtpSenderJob).to receive(:perform_later) + allow(SmsOtpSenderJob).to receive(:perform_later) + + update_phone_number(guam_phone) + + expect(current_path).to eq login_two_factor_path(otp_delivery_preference: :sms) + expect(VoiceOtpSenderJob).to_not have_received(:perform_later) + expect(SmsOtpSenderJob).to have_received(:perform_later) + expect(page).to_not have_content(t('links.two_factor_authentication.resend_code.phone')) + end + scenario 'waiting too long to change phone number' do allow(SmsOtpSenderJob).to receive(:perform_later) @@ -154,11 +172,13 @@ def complete_2fa_confirmation_without_entering_otp fill_in 'Password', with: Features::SessionHelper::VALID_PASSWORD click_button t('forms.buttons.continue') - expect(current_path).to eq login_two_factor_path(otp_delivery_preference: 'sms') + expect(current_path).to eq login_two_factor_path( + otp_delivery_preference: user.otp_delivery_preference + ) end - def update_phone_number - fill_in 'update_user_phone_form[phone]', with: '703-555-0100' + def update_phone_number(phone = '703-555-0100') + fill_in 'update_user_phone_form[phone]', with: phone click_button t('forms.buttons.submit.confirm_change') end diff --git a/spec/features/two_factor_authentication/sign_in_spec.rb b/spec/features/two_factor_authentication/sign_in_spec.rb index b3e116f5427..7ce7aff8dd1 100644 --- a/spec/features/two_factor_authentication/sign_in_spec.rb +++ b/spec/features/two_factor_authentication/sign_in_spec.rb @@ -32,6 +32,104 @@ expect(user.reload.phone).to_not eq '+1 (555) 555-1212' expect(user.voice?).to eq true end + + context 'with U.S. phone that does not support phone delivery method' do + let(:guam_phone) { '671-555-5555' } + + scenario 'renders an error if a user submits with phone selected' do + sign_in_before_2fa + fill_in 'Phone', with: guam_phone + choose 'Phone call' + click_send_security_code + + expect(current_path).to eq(phone_setup_path) + expect(page).to have_content t( + 'devise.two_factor_authentication.otp_delivery_preference.phone_unsupported', + location: 'Guam' + ) + end + + scenario 'disables the phone option and displays a warning with js', :js do + sign_in_before_2fa + + fill_in 'Phone', with: guam_phone + phone_radio_button = page.find( + '#two_factor_setup_form_otp_delivery_preference_voice', + visible: :all + ) + + expect(page).to have_content t( + 'devise.two_factor_authentication.otp_delivery_preference.phone_unsupported', + location: 'Guam' + ) + expect(phone_radio_button).to be_disabled + + fill_in 'Phone', with: '555-555-5000' + + expect(page).not_to have_content t( + 'devise.two_factor_authentication.otp_delivery_preference.phone_unsupported', + location: 'Guam' + ) + expect(phone_radio_button).to_not be_disabled + end + end + + context 'with international phone that does not support phone delivery' do + scenario 'renders an error if a user submits with phone selected' do + sign_in_before_2fa + select 'Turkey +90', from: 'International code' + fill_in 'Phone', with: '555-555-5000' + choose 'Phone call' + click_send_security_code + + expect(current_path).to eq(phone_setup_path) + expect(page).to have_content t( + 'devise.two_factor_authentication.otp_delivery_preference.phone_unsupported', + location: 'Turkey' + ) + end + + scenario 'disables the phone option and displays a warning with js', :js do + sign_in_before_2fa + select 'Turkey +90', from: 'International code' + fill_in 'Phone', with: '+90 312 213 29 65' + phone_radio_button = page.find( + '#two_factor_setup_form_otp_delivery_preference_voice', + visible: :all + ) + + expect(page).to have_content t( + 'devise.two_factor_authentication.otp_delivery_preference.phone_unsupported', + location: 'Turkey' + ) + expect(phone_radio_button).to be_disabled + + select 'Canada +1', from: 'International code' + + expect(page).not_to have_content t( + 'devise.two_factor_authentication.otp_delivery_preference.phone_unsupported', + location: 'Turkey' + ) + expect(phone_radio_button).to_not be_disabled + end + + scenario 'updates international code as user types', :js do + sign_in_before_2fa + fill_in 'Phone', with: '+81 54 354 3643' + + expect(page.find('#two_factor_setup_form_international_code').value).to eq 'JP' + + fill_in 'Phone', with: '5376' + select 'Morocco +212', from: 'International code' + + expect(find('#two_factor_setup_form_phone').value).to eq '+212 5376' + + fill_in 'Phone', with: '54354' + select 'Japan +81', from: 'International code' + + expect(find('#two_factor_setup_form_phone').value).to include '+81' + end + end end def attempt_to_bypass_2fa_setup @@ -111,6 +209,25 @@ def submit_prefilled_otp_code expect(page.evaluate_script('document.activeElement.id')).to eq 'code' end + scenario 'the user changes delivery method' do + user = create(:user, :signed_up, otp_delivery_preference: :sms) + sign_in_before_2fa(user) + + allow(VoiceOtpSenderJob).to receive(:perform_later) + + click_on t('links.two_factor_authentication.voice') + + expect(VoiceOtpSenderJob).to have_received(:perform_later) + end + + scenario 'the user cannot change delivery method if phone is unsupported' do + guam_phone = '+1 (671) 555-5000' + user = create(:user, :signed_up, phone: guam_phone) + sign_in_before_2fa(user) + + expect(page).to_not have_link t('links.two_factor_authentication.voice') + end + context 'user enters OTP incorrectly 3 times', js: true do it 'locks the user out and leaves user on the page during entire lockout period' do allow(Figaro.env).to receive(:session_check_frequency).and_return('0') diff --git a/spec/features/users/password_recovery_via_recovery_code_spec.rb b/spec/features/users/password_recovery_via_recovery_code_spec.rb index e3be9e3c66f..d220b1975be 100644 --- a/spec/features/users/password_recovery_via_recovery_code_spec.rb +++ b/spec/features/users/password_recovery_via_recovery_code_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -feature 'Password recovery via personal key' do +feature 'Password recovery via personal key', idv_job: true do include PersonalKeyHelper include IdvHelper diff --git a/spec/features/users/sign_in_spec.rb b/spec/features/users/sign_in_spec.rb index a32ac94f187..7ba57d8ecb5 100644 --- a/spec/features/users/sign_in_spec.rb +++ b/spec/features/users/sign_in_spec.rb @@ -246,4 +246,17 @@ expect(page).to have_content t('devise.failure.invalid') end end + + context 'invalid request_id' do + it 'allows the user to sign in and does not try to redirect to any SP' do + allow(FeatureManagement).to receive(:prefill_otp_codes?).and_return(true) + user = create(:user, :signed_up) + + visit new_user_session_path(request_id: 'invalid') + fill_in_credentials_and_submit(user.email, user.password) + click_submit_default + + expect(current_path).to eq account_path + end + end end diff --git a/spec/features/users/user_edit_spec.rb b/spec/features/users/user_edit_spec.rb index e579517d487..90810805be6 100644 --- a/spec/features/users/user_edit_spec.rb +++ b/spec/features/users/user_edit_spec.rb @@ -1,23 +1,47 @@ require 'rails_helper' feature 'User edit' do - scenario 'user sees error message if form is submitted without email', js: true do - sign_in_and_2fa_user + context 'editing email' do + before do + sign_in_and_2fa_user + visit manage_email_path + end - visit manage_email_path - fill_in 'Email', with: '' - click_button 'Update' + scenario 'user sees error message if form is submitted without email', :js, idv_job: true do + fill_in 'Email', with: '' + click_button 'Update' - expect(page).to have_content t('valid_email.validations.email.invalid') + expect(page).to have_content t('valid_email.validations.email.invalid') + end end - scenario 'user sees error message if form is submitted without phone number', js: true do - sign_in_and_2fa_user + context 'editing 2FA phone number' do + before do + sign_in_and_2fa_user + visit manage_phone_path + end - visit manage_phone_path - fill_in 'Phone', with: '' - click_button t('forms.buttons.submit.confirm_change') + scenario 'user sees error message if form is submitted without phone number', js: true do + fill_in 'Phone', with: '' + click_button t('forms.buttons.submit.confirm_change') - expect(page).to have_content t('errors.messages.improbable_phone') + expect(page).to have_content t('errors.messages.improbable_phone') + end + + scenario 'updates international code as user types', :js do + fill_in 'Phone', with: '+81 54 354 3643' + + expect(page.find('#update_user_phone_form_international_code').value).to eq 'JP' + + fill_in 'Phone', with: '5376' + select 'Morocco +212', from: 'International code' + + expect(find('#update_user_phone_form_phone').value).to eq '+212 5376' + + fill_in 'Phone', with: '54354' + select 'Japan +81', from: 'International code' + + expect(find('#update_user_phone_form_phone').value).to include '+81' + end end end diff --git a/spec/features/users/user_profile_spec.rb b/spec/features/users/user_profile_spec.rb index e04ded54ba7..89d310f47dc 100644 --- a/spec/features/users/user_profile_spec.rb +++ b/spec/features/users/user_profile_spec.rb @@ -66,7 +66,7 @@ expect(page).to have_content(t('idv.messages.personal_key')) end - it 'allows the user reactivate their profile by reverifying' do + it 'allows the user reactivate their profile by reverifying', idv_job: true do profile = create(:profile, :active, :verified, pii: { ssn: '1234', dob: '1920-01-01' }) user = profile.user diff --git a/spec/features/visitors/i18n_spec.rb b/spec/features/visitors/i18n_spec.rb index e86cf30231c..3d28cc19c02 100644 --- a/spec/features/visitors/i18n_spec.rb +++ b/spec/features/visitors/i18n_spec.rb @@ -1,7 +1,14 @@ require 'rails_helper' feature 'Internationalization' do - context 'visit homepage' do + context 'visit homepage with no locale set' do + it 'displays a header in the default locale' do + visit root_path + expect(page).to have_content t('headings.sign_in_without_sp', locale: 'en') + end + end + + context 'visit homepage with locale set in header' do before do page.driver.header 'Accept-Language', locale visit root_path @@ -22,5 +29,50 @@ expect(page).to have_content t('headings.sign_in_without_sp', locale: 'es') end end + + context 'when the user selects an unsupported locale' do + let(:locale) { :es } + + it 'it does not raise an exception' do + expect { visit root_path + '?locale=foo' }.to_not raise_exception + end + + it 'it falls back to the locale set in header' do + expect(page).to have_content t('headings.sign_in_without_sp', locale: 'es') + end + end + end + + context 'visit homepage without a locale param set' do + it 'displays header in the default locale' do + visit '/' + + expect(page).to have_content t('headings.sign_in_without_sp', locale: 'en') + end + + it 'allows user to manually toggle language from dropdown menu', js: true do + visit root_path + within(:css, '.i18n-desktop-dropdown', visible: false) do + find_link(t('i18n.locale.es'), visible: false).trigger('click') + end + + expect(page).to have_content t('headings.sign_in_without_sp', locale: 'es') + expect(page).to have_content t('i18n.language', locale: 'es') + + within(:css, '.i18n-desktop-dropdown', visible: false) do + find_link(t('i18n.locale.en'), visible: false).trigger('click') + end + + expect(page).to have_content t('headings.sign_in_without_sp', locale: 'en') + expect(page).to have_content t('i18n.language', locale: 'en') + end + end + + context 'visit homepage with locale param set to :es' do + it 'displays a translated header to the user' do + visit '/es/' + + expect(page).to have_content t('headings.sign_in_without_sp', locale: 'es') + end end end diff --git a/spec/features/visitors/phone_confirmation_spec.rb b/spec/features/visitors/phone_confirmation_spec.rb index b32f556c6c7..6b588a49fcd 100644 --- a/spec/features/visitors/phone_confirmation_spec.rb +++ b/spec/features/visitors/phone_confirmation_spec.rb @@ -47,7 +47,7 @@ it 'informs the user that the OTP code is sent to the phone' do expect(page).to have_content( - t('instructions.2fa.sms.confirm_code_html', + t('instructions.mfa.sms.confirm_code_html', number: '+1 (555) 555-5555', resend_code_link: t('links.two_factor_authentication.resend_code.sms')) ) @@ -65,7 +65,7 @@ it 'pretends the phone is valid and prompts to confirm the number' do expect(current_path).to eq login_two_factor_path(otp_delivery_preference: 'sms') expect(page).to have_content( - t('instructions.2fa.sms.confirm_code_html', + t('instructions.mfa.sms.confirm_code_html', number: @existing_user.phone, resend_code_link: t('links.two_factor_authentication.resend_code.sms')) ) diff --git a/spec/features/visitors/set_password_spec.rb b/spec/features/visitors/set_password_spec.rb index 8a518521deb..ad3efba140c 100644 --- a/spec/features/visitors/set_password_spec.rb +++ b/spec/features/visitors/set_password_spec.rb @@ -55,7 +55,7 @@ expect(page).to have_content '...' fill_in 'password_form_password', with: 'password' - expect(page).to have_content 'This is a top-10 common password' + expect(page).to have_content t('zxcvbn.feedback.this_is_a_top_10_common_password') end end @@ -92,7 +92,7 @@ click_button t('forms.buttons.continue') - expect(page).to have_content t('zxcvbn.feedback.This is a top-10 common password') + expect(page).to have_content t('zxcvbn.feedback.this_is_a_top_10_common_password') end end end diff --git a/spec/forms/idv/finance_form_spec.rb b/spec/forms/idv/finance_form_spec.rb index a2b0411a10a..2c2a027da57 100644 --- a/spec/forms/idv/finance_form_spec.rb +++ b/spec/forms/idv/finance_form_spec.rb @@ -25,39 +25,58 @@ end describe '#submit' do - it 'adds ccn key to idv_params when valid' do - expect(subject.submit(ccn: '12345678', finance_type: :ccn)).to eq true + context 'when the form is valid' do + let(:result) { subject.submit(ccn: '12345678', finance_type: :ccn) } - expected_params = { - ccn: '12345678', - } + it 'returns a successful form response' do + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(true) + expect(result.errors).to be_empty + end - expect(subject.idv_params).to eq expected_params + it 'adds ccn key to idv_params' do + expected_params = { + ccn: '12345678', + } + subject.submit(ccn: '12345678', finance_type: :ccn) + expect(subject.idv_params).to eq expected_params + end end - it 'fails when missing all finance fields' do - expect(subject.submit(foo: 'bar')).to eq false + context 'when the form is invalid' do + it 'returns an unsuccessful form response' do + result = subject.submit(foo: 'bar') + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(false) + expect(result.errors).to be_present + end end context 'when CCN is invalid' do - it 'fails when alpha' do - expect(subject.submit(ccn: '1234567a', finance_type: :ccn)).to eq false - expect(subject.errors[:ccn]).to eq([t('idv.errors.invalid_ccn')]) + it 'returns an unsuccessful form response when alpha' do + result = subject.submit(ccn: '1234567a', finance_type: :ccn) + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(false) + expect(result.errors[:ccn]).to eq([t('idv.errors.invalid_ccn')]) end - it 'fails when long' do - expect(subject.submit(ccn: '123456789', finance_type: :ccn)).to eq false - expect(subject.errors[:ccn]).to eq([t('idv.errors.invalid_ccn')]) + it 'returns an unsuccessful form response when long' do + result = subject.submit(ccn: '123456789', finance_type: :ccn) + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(false) + expect(result.errors[:ccn]).to eq([t('idv.errors.invalid_ccn')]) end - it 'fails when short' do - expect(subject.submit(ccn: '1234567', finance_type: :ccn)).to eq false - expect(subject.errors[:ccn]).to eq([t('idv.errors.invalid_ccn')]) + it 'returns an unsuccessful form response when short' do + result = subject.submit(ccn: '1234567', finance_type: :ccn) + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(false) + expect(result.errors[:ccn]).to eq([t('idv.errors.invalid_ccn')]) end end context 'any non-ccn financial value is less than the minimum allowed digits' do - it 'fails' do + it 'returns an unsuccessful form response' do finance_types = Idv::FinanceForm::FINANCE_TYPES short_value = '1' * (FormFinanceValidator::VALID_MINIMUM_LENGTH - 1) @@ -65,8 +84,10 @@ next if type == :ccn params = { type => short_value, finance_type: type } - expect(subject.submit(params)).to eq false - expect(subject.errors[type]).to eq([t( + result = subject.submit(params) + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(false) + expect(result.errors[type]).to eq([t( 'idv.errors.finance_number_length', minimum: FormFinanceValidator::VALID_MINIMUM_LENGTH, maximum: FormFinanceValidator::VALID_MAXIMUM_LENGTH @@ -76,7 +97,7 @@ end context 'any non-ccn financial value is over the max allowed digits' do - it 'fails' do + it 'returns an unsuccessful form response' do finance_types = Idv::FinanceForm::FINANCE_TYPES long_value = '1' * (FormFinanceValidator::VALID_MAXIMUM_LENGTH + 1) @@ -88,8 +109,10 @@ finance_type: symbolized_type, } - expect(subject.submit(params)).to eq false - expect(subject.errors[symbolized_type]).to eq([t( + result = subject.submit(params) + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(false) + expect(result.errors[symbolized_type]).to eq([t( 'idv.errors.finance_number_length', minimum: FormFinanceValidator::VALID_MINIMUM_LENGTH, maximum: FormFinanceValidator::VALID_MAXIMUM_LENGTH diff --git a/spec/forms/idv/phone_form_spec.rb b/spec/forms/idv/phone_form_spec.rb index 7eb9673c2a1..0dcf1a3bf25 100644 --- a/spec/forms/idv/phone_form_spec.rb +++ b/spec/forms/idv/phone_form_spec.rb @@ -7,21 +7,41 @@ it_behaves_like 'a phone form' describe '#submit' do - it 'adds phone key to idv_params when valid' do - subject.submit(phone: '703-555-1212') + context 'when the form is valid' do + it 'returns a successful form response' do + result = subject.submit(phone: '703-555-1212', international_code: 'US') - expected_params = { - phone: '+1 (703) 555-1212', - } + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(true) + expect(result.errors).to be_empty + end - expect(subject.idv_params).to eq expected_params + it 'adds phone key to idv_params' do + subject.submit(phone: '703-555-1212', international_code: 'US') + + expected_params = { + phone: '7035551212', + } + + expect(subject.idv_params).to eq expected_params + end + end + + context 'when the form is invalid' do + it 'returns an unsuccessful form response' do + result = subject.submit(phone: 'Im not a phone number 🙃', international_code: 'US') + + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(false) + expect(result.errors).to include(:phone) + end end it 'adds phone_confirmed_at key to idv_params when submitted phone equals user phone' do - subject.submit(phone: '+1 (202) 555-1212') + subject.submit(phone: '+1 (202) 555-1212', international_code: 'US') expected_params = { - phone: user.phone, + phone: '2025551212', phone_confirmed_at: user.phone_confirmed_at, } diff --git a/spec/forms/idv/profile_form_spec.rb b/spec/forms/idv/profile_form_spec.rb index 60a8b935e7e..3c0987ef2ad 100644 --- a/spec/forms/idv/profile_form_spec.rb +++ b/spec/forms/idv/profile_form_spec.rb @@ -19,6 +19,28 @@ } end + describe '#submit' do + context 'when the form is valid' do + it 'returns a successful form response' do + result = subject.submit(profile_attrs) + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(true) + expect(result.errors).to be_empty + end + end + + context 'when the form is invalid' do + before { profile_attrs[:dob] = nil } + + it 'returns an unsuccessful form response' do + result = subject.submit(profile_attrs) + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(false) + expect(result.errors).to include(:dob) + end + end + end + describe 'presence validations' do it 'is invalid when required attribute is not present' do %i[first_name last_name ssn dob address1 city state zipcode].each do |attr| diff --git a/spec/forms/otp_delivery_selection_form_spec.rb b/spec/forms/otp_delivery_selection_form_spec.rb index 0441b6cf884..e50d2687ae5 100644 --- a/spec/forms/otp_delivery_selection_form_spec.rb +++ b/spec/forms/otp_delivery_selection_form_spec.rb @@ -1,7 +1,8 @@ require 'rails_helper' describe OtpDeliverySelectionForm do - subject { OtpDeliverySelectionForm.new(build_stubbed(:user)) } + let(:phone_to_deliver_to) { '+1 (202) 555-1234' } + subject { OtpDeliverySelectionForm.new(build_stubbed(:user), phone_to_deliver_to) } describe 'otp_delivery_preference inclusion validation' do it 'is invalid when otp_delivery_preference is neither sms nor voice' do @@ -18,6 +19,8 @@ extra = { otp_delivery_preference: 'sms', resend: true, + country_code: '1', + area_code: '202', } result = instance_double(FormResponse) @@ -35,6 +38,8 @@ extra = { otp_delivery_preference: 'foo', resend: nil, + country_code: '1', + area_code: '202', } result = instance_double(FormResponse) @@ -48,7 +53,7 @@ context 'when otp_delivery_preference is the same as the user otp_delivery_preference' do it 'does not update the user' do user = build_stubbed(:user, otp_delivery_preference: 'sms') - form = OtpDeliverySelectionForm.new(user) + form = OtpDeliverySelectionForm.new(user, phone_to_deliver_to) expect(UpdateUser).to_not receive(:new) @@ -59,7 +64,7 @@ context 'when otp_delivery_preference is different from the user otp_delivery_preference' do it 'updates the user' do user = build_stubbed(:user, otp_delivery_preference: 'voice') - form = OtpDeliverySelectionForm.new(user) + form = OtpDeliverySelectionForm.new(user, phone_to_deliver_to) attributes = { otp_delivery_preference: 'sms' } updated_user = instance_double(UpdateUser) diff --git a/spec/forms/two_factor_setup_form_spec.rb b/spec/forms/two_factor_setup_form_spec.rb index 515a318f1c7..7af36cef7a6 100644 --- a/spec/forms/two_factor_setup_form_spec.rb +++ b/spec/forms/two_factor_setup_form_spec.rb @@ -2,9 +2,19 @@ describe TwoFactorSetupForm, type: :model do let(:user) { build_stubbed(:user) } - let(:valid_phone) { '+1 (202) 202-2020' } + let(:phone) { '+1 (202) 202-2020' } + let(:international_code) { 'US' } + let(:params) do + { + phone: phone, + international_code: 'US', + otp_delivery_preference: 'sms', + } + end subject { TwoFactorSetupForm.new(user) } + it_behaves_like 'an otp delivery preference form' + it do is_expected. to validate_presence_of(:phone). @@ -12,12 +22,28 @@ end describe 'phone validation' do - it 'uses the phony_rails gem with country option set to US' do + it 'uses the phony_rails gem' do phone_validator = subject._validators.values.flatten. detect { |v| v.class == PhonyPlausibleValidator } - expect(phone_validator.options). - to eq(country_code: 'US', presence: true, message: :improbable_phone) + expect(phone_validator.options[:presence]).to eq(true) + expect(phone_validator.options[:message]).to eq(:improbable_phone) + expect(phone_validator.options).to include(:international_code) + end + + it do + should validate_inclusion_of(:international_code). + in_array(PhoneNumberCapabilities::INTERNATIONAL_CODES.keys) + end + + it 'validates that the number matches the requested international code' do + params[:phone] = '123 123 1234' + params[:international_code] = 'MA' + result = subject.submit(params) + + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(false) + expect(result.errors).to include(:phone) end end @@ -32,14 +58,17 @@ } result = instance_double(FormResponse) + params[:phone] = user.phone + expect(FormResponse).to receive(:new). with(success: true, errors: {}, extra: extra).and_return(result) - expect(form.submit(phone: user.phone, otp_delivery_preference: 'sms')). - to eq result + expect(form.submit(params)).to eq result end end context 'when phone is not already taken' do + let(:phone) { '+1 (703) 555-1212' } + it 'is valid' do extra = { otp_delivery_preference: 'sms', @@ -48,14 +77,13 @@ expect(FormResponse).to receive(:new). with(success: true, errors: {}, extra: extra).and_return(result) - expect(subject.submit(phone: '+1 (703) 555-1212', otp_delivery_preference: 'sms')). - to eq result + expect(subject.submit(params)).to eq result end end context 'when phone is same as current user' do it 'is valid' do - user = build_stubbed(:user, phone: valid_phone) + user = build_stubbed(:user, phone: phone) form = TwoFactorSetupForm.new(user) extra = { otp_delivery_preference: 'sms', @@ -64,12 +92,13 @@ expect(FormResponse).to receive(:new). with(success: true, errors: {}, extra: extra).and_return(result) - expect(form.submit(phone: valid_phone, otp_delivery_preference: 'sms')). - to eq result + expect(form.submit(params)).to eq result end end context 'when phone is empty' do + let(:phone) { '' } + it 'does not add already taken errors' do errors = { phone: [t('errors.messages.improbable_phone')], @@ -81,8 +110,7 @@ expect(FormResponse).to receive(:new). with(success: false, errors: errors, extra: extra).and_return(result) - expect(subject.submit(phone: '', otp_delivery_preference: 'sms')). - to eq result + expect(subject.submit(params)).to eq result end end end @@ -95,7 +123,7 @@ expect(UpdateUser).to_not receive(:new) - form.submit(phone: '+1 (703) 555-1212', otp_delivery_preference: 'sms') + form.submit(params) end end @@ -111,7 +139,7 @@ expect(updated_user).to receive(:call) - form.submit(phone: '+1 (703) 555-1212', otp_delivery_preference: 'sms') + form.submit(params) end end end diff --git a/spec/helpers/locale_helper_spec.rb b/spec/helpers/locale_helper_spec.rb new file mode 100644 index 00000000000..de3dc7818d1 --- /dev/null +++ b/spec/helpers/locale_helper_spec.rb @@ -0,0 +1,31 @@ +require 'rails_helper' + +RSpec.describe LocaleHelper do + include LocaleHelper + + describe '#locale_url_param' do + context 'in the default locale' do + before { I18n.locale = :en } + + it 'is nil' do + expect(locale_url_param).to be_nil + end + end + + context 'in French (a non-default locale)' do + before { I18n.locale = :fr } + + it 'is that locale' do + expect(locale_url_param).to eq(:fr) + end + end + + context 'in Spanish (a non-default locale)' do + before { I18n.locale = :es } + + it 'is that locale' do + expect(locale_url_param).to eq(:es) + end + end + end +end diff --git a/spec/i18n_spec.rb b/spec/i18n_spec.rb index 8dd0319a723..4c753acbc9b 100644 --- a/spec/i18n_spec.rb +++ b/spec/i18n_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'i18n/tasks' +require 'yaml_normalizer' RSpec.describe 'I18n' do let(:i18n) { I18n::Tasks::BaseTask.new } @@ -20,4 +21,59 @@ "#{unused_keys.leaves.count} unused i18n keys, run `i18n-tasks unused' to show them" ) end + + root_dir = File.expand_path(File.join(File.dirname(__FILE__), '../')) + + Dir[File.join(root_dir, '/config/locales/**/*.yml')].each do |full_path| + i18n_file = full_path.sub("#{root_dir}/", '') + + describe i18n_file do + it 'has only lower_snake_case keys' do + keys = flatten_hash(YAML.load_file(full_path)).keys + + bad_keys = keys.reject { |key| key =~ /^[a-z0-9_.]+$/ } + + expect(bad_keys).to be_empty + end + + it 'has only has XML-safe identifiers (keys start with a letter)' do + keys = flatten_hash(YAML.load_file(full_path)).keys + + bad_keys = keys.select { |key| key.split('.').any? { |part| part =~ /^[0-9]/ } } + + expect(bad_keys).to be_empty + end + + it 'has correctly-formatted interpolation values' do + bad_keys = flatten_hash(YAML.load_file(full_path)).select do |_key, value| + next unless value.is_a?(String) + + interpolation_names = value.scan(/%\{([^\}]+)\}/).flatten + + interpolation_names.any? { |name| name.downcase != name } + end + + expect(bad_keys).to be_empty + end + + it 'is formatted as normalized YAML (run scripts/normalize-yaml)' do + normalized_yaml = YAML.dump(YamlNormalizer.chomp_each(YAML.load_file(full_path))) + + expect(File.read(full_path)).to eq(normalized_yaml) + end + end + end + + def flatten_hash(hash, parent_keys: [], out_hash: {}, &block) + hash.each do |key, value| + if value.is_a?(Hash) + flatten_hash(value, parent_keys: parent_keys + [key], out_hash: out_hash, &block) + else + flat_key = [*parent_keys, key].join('.') + out_hash[flat_key] = value + end + end + + out_hash + end end diff --git a/spec/jobs/sms_otp_sender_job_spec.rb b/spec/jobs/sms_otp_sender_job_spec.rb index 1afc5369d23..a76fb301a4d 100644 --- a/spec/jobs/sms_otp_sender_job_spec.rb +++ b/spec/jobs/sms_otp_sender_job_spec.rb @@ -3,14 +3,26 @@ describe SmsOtpSenderJob do describe '.perform' do - it 'sends a message containing the OTP code to the mobile number', twilio: true do + before do + reset_job_queues TwilioService.telephony_service = FakeSms + FakeSms.messages = [] + end + subject(:perform) do SmsOtpSenderJob.perform_now( code: '1234', - phone: '555-5555', - otp_created_at: Time.zone.now.to_s + phone: '+1 (888) 555-5555', + otp_created_at: otp_created_at ) + end + + let(:otp_created_at) { Time.zone.now.to_s } + + it 'sends a message containing the OTP code to the mobile number', twilio: true do + TwilioService.telephony_service = FakeSms + + perform messages = FakeSms.messages @@ -19,43 +31,49 @@ msg = messages.first expect(msg.from).to match(/(\+19999999999|\+12222222222)/) - expect(msg.to).to eq('555-5555') - expect(msg.body).to include('one-time security code') - expect(msg.body).to include('1234') + expect(msg.to).to eq('+1 (888) 555-5555') + expect(msg.body).to eq(I18n.t('jobs.sms_otp_sender_job.message', code: '1234', app: APP_NAME)) end - it 'does not send if the OTP code is expired' do - reset_job_queues - TwilioService.telephony_service = FakeSms - FakeSms.messages = [] - otp_expiration_period = Devise.direct_otp_valid_for + context 'if the OTP code is expired' do + let(:otp_created_at) do + otp_expiration_period = Devise.direct_otp_valid_for + otp_expiration_period.ago.to_s + end - SmsOtpSenderJob.perform_now( - code: '1234', - phone: '555-5555', - otp_created_at: otp_expiration_period.ago.to_s - ) + it 'does not send if the OTP code is expired' do + perform - messages = FakeSms.messages - expect(messages.size).to eq(0) - expect(ActiveJob::Base.queue_adapter.enqueued_jobs).to eq [] + messages = FakeSms.messages + expect(messages.size).to eq(0) + expect(ActiveJob::Base.queue_adapter.enqueued_jobs).to eq [] + end end - it 'respects time zone' do - reset_job_queues - TwilioService.telephony_service = FakeSms - FakeSms.messages = [] - otp_expiration_period = Devise.direct_otp_valid_for + context 'in other time zones' do + let(:otp_created_at) do + otp_expiration_period = Devise.direct_otp_valid_for + otp_expiration_period.ago.strftime('%F %r') + end - SmsOtpSenderJob.perform_now( - code: '1234', - phone: '555-5555', - otp_created_at: otp_expiration_period.ago.strftime('%F %r') - ) + it 'respects time zone' do + perform - messages = FakeSms.messages - expect(messages.size).to eq(0) - expect(ActiveJob::Base.queue_adapter.enqueued_jobs).to eq [] + messages = FakeSms.messages + expect(messages.size).to eq(0) + expect(ActiveJob::Base.queue_adapter.enqueued_jobs).to eq [] + end + end + + it 'sanitizes phone numbers embedded in error messages from Twilio' do + raw_message = "The 'To' number +1 (888) 555-5555 is not a valid phone number" + sanitized_message = "The 'To' number +# (###) ###-#### is not a valid phone number" + + expect_any_instance_of(TwilioService).to receive(:send_sms). + and_raise(Twilio::REST::RequestError.new(raw_message)) + + expect { perform }. + to raise_error(Twilio::REST::RequestError, sanitized_message) end end end diff --git a/spec/jobs/vendor_validator_job_spec.rb b/spec/jobs/vendor_validator_job_spec.rb new file mode 100644 index 00000000000..8e73c937a69 --- /dev/null +++ b/spec/jobs/vendor_validator_job_spec.rb @@ -0,0 +1,67 @@ +require 'rails_helper' + +RSpec.describe VendorValidatorJob do + let(:result_id) { SecureRandom.uuid } + let(:vendor_validator_class) { 'Idv::PhoneValidator' } + let(:vendor) { :mock } + let(:vendor_params) { '+1 (888) 123-4567' } + let(:applicant) { Proofer::Applicant.new(first_name: 'Test') } + let(:applicant_json) { applicant.to_json } + let(:vendor_session_id) { SecureRandom.uuid } + + subject(:job) { VendorValidatorJob.new } + + describe '#perform' do + subject(:perform) do + job.perform( + result_id: result_id, + vendor_validator_class: vendor_validator_class, + vendor: vendor, + vendor_params: vendor_params, + applicant_json: applicant_json, + vendor_session_id: vendor_session_id + ) + end + + it 'calls out to a vendor and serializes the result' do + expect(Idv::PhoneValidator).to receive(:new). + with( + applicant: kind_of(Proofer::Applicant), + vendor: vendor, + vendor_params: vendor_params, + vendor_session_id: vendor_session_id + ).and_call_original + + before_result = VendorValidatorResultStorage.new.load(result_id) + expect(before_result).to be_nil + + perform + + after_result = VendorValidatorResultStorage.new.load(result_id) + expect(after_result).to be_a(Idv::VendorResult) + end + + context 'when the vendor throws an exception' do + let(:vendor_validator_class) { 'Idv::ProfileValidator' } + let(:applicant) { Proofer::Applicant.new(first_name: 'Fail') } + + let(:exception_msg) { 'Failed to contact proofing vendor' } + + it 'notifies NewRelic and does not raise' do + expect(NewRelic::Agent).to receive(:notice_error). + with(kind_of(StandardError)) + + perform + end + + it 'writes a failure result to redis' do + perform + + result = VendorValidatorResultStorage.new.load(result_id) + expect(result.success?).to eq(false) + expect(result.errors).to eq(agent: [exception_msg]) + expect(result.reasons).to eq([exception_msg]) + end + end + end +end diff --git a/spec/lib/feature_management_spec.rb b/spec/lib/feature_management_spec.rb index fbdd9c8df5e..676fe6ef723 100644 --- a/spec/lib/feature_management_spec.rb +++ b/spec/lib/feature_management_spec.rb @@ -159,4 +159,49 @@ end end end + + describe '.no_pii_mode?' do + let(:proofing_vendor) { :mock } + let(:enable_identity_verification) { false } + + before do + allow_any_instance_of(Idv::Vendor).to receive(:pick).and_return(proofing_vendor) + allow(Figaro.env).to receive(:enable_identity_verification). + and_return(enable_identity_verification.to_json) + end + + subject(:no_pii_mode?) { FeatureManagement.no_pii_mode? } + + context 'with mock ID-proofing vendors' do + let(:proofing_vendor) { :mock } + + context 'with identity verification enabled' do + let(:enable_identity_verification) { true } + + it { expect(no_pii_mode?).to eq(true) } + end + + context 'with identity verification disabled' do + let(:enable_identity_verification) { false } + + it { expect(no_pii_mode?).to eq(false) } + end + end + + context 'with real ID-proofing vendors' do + let(:proofing_vendor) { :not_mock } + + context 'with identity verification enabled' do + let(:enable_identity_verification) { true } + + it { expect(no_pii_mode?).to eq(false) } + end + + context 'with identity verification disabled' do + let(:enable_identity_verification) { false } + + it { expect(no_pii_mode?).to eq(false) } + end + end + end end diff --git a/spec/lib/yaml_normalizer_spec.rb b/spec/lib/yaml_normalizer_spec.rb new file mode 100644 index 00000000000..3ee77d157be --- /dev/null +++ b/spec/lib/yaml_normalizer_spec.rb @@ -0,0 +1,95 @@ +require 'spec_helper' +require 'yaml_normalizer' + +RSpec.describe YamlNormalizer do + describe '.run' do + let(:tempfile) { Tempfile.new } + + before do + File.open(tempfile.path, 'w') do |f| + f.puts <<~YAML + some: + key: > + quoted + value1: 'quoted' + value2: "quoted" + YAML + end + end + + after { tempfile.unlink } + + it 'normalizes a YAML files in-place' do + YamlNormalizer.run([tempfile.path]) + + expect(File.read(tempfile.path)).to eq <<~YAML + --- + some: + key: quoted + value1: quoted + value2: quoted + YAML + end + end + + describe '.chomp_each' do + context 'trailing newlines' do + let(:original) do + { + key: 'a: ', + array: %W[b\n c\n], + nested: { + value: "d\n", + }, + } + end + + let(:trimmed) do + { + key: 'a: ', + array: %w[b c], + nested: { value: 'd' }, + } + end + + it 'in-place, recursively trims trailing newlines from all strings in a hash' do + YamlNormalizer.chomp_each(original) + + expect(original).to eq(trimmed) + end + end + + context 'trailing spaces' do + let(:original) { { a: 'a : ', b: 'b ', c: "c : \n" } } + let(:trimmed) { { a: 'a : ', b: 'b', c: 'c : ' } } + + it 'trims trailing spaces, except after a colon' do + YamlNormalizer.chomp_each(original) + + expect(original).to eq(trimmed) + end + end + + context 'leading newlines' do + let(:original) { { a: "\n\na b c", b: "a\nb" } } + let(:trimmed) { { a: "a b c", b: "a\nb" } } + + it 'trims leading newlines but not intermediate ones' do + YamlNormalizer.chomp_each(original) + + expect(original).to eq(trimmed) + end + end + + context 'a nil value' do + let(:original) { { a: nil } } + let(:trimmed) { { a: nil } } + + it 'does not blow up' do + YamlNormalizer.chomp_each(original) + + expect(original).to eq(trimmed) + end + end + end +end diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb index cfce7ebf52d..c3b16d2cd8a 100644 --- a/spec/mailers/user_mailer_spec.rb +++ b/spec/mailers/user_mailer_spec.rb @@ -67,6 +67,14 @@ ) expect_email_body_to_have_help_and_contact_links end + + context 'in a non-default locale' do + before { I18n.locale = :fr } + + it 'links to the correct locale' do + expect(mail.html_part.body).to include(root_url(locale: :fr)) + end + end end describe 'phone_changed' do diff --git a/spec/models/otp_requests_tracker_spec.rb b/spec/models/otp_requests_tracker_spec.rb new file mode 100644 index 00000000000..21c0f8fdde1 --- /dev/null +++ b/spec/models/otp_requests_tracker_spec.rb @@ -0,0 +1,51 @@ +require 'rails_helper' + +describe OtpRequestsTracker do + describe '.find_or_create_with_phone' do + let(:phone) { '+1 703 555 1212' } + let(:phone_fingerprint) { Pii::Fingerprinter.fingerprint(phone) } + + context 'match found' do + it 'returns the existing record and does not change it' do + OtpRequestsTracker.create( + phone_fingerprint: phone_fingerprint, + otp_send_count: 3, + otp_last_sent_at: Time.zone.now - 1.hour + ) + + existing = OtpRequestsTracker.where(phone_fingerprint: phone_fingerprint).first + + expect { OtpRequestsTracker.find_or_create_with_phone(phone) }. + to_not change(OtpRequestsTracker, :count) + expect { OtpRequestsTracker.find_or_create_with_phone(phone) }. + to_not change { existing.otp_send_count } + expect { OtpRequestsTracker.find_or_create_with_phone(phone) }. + to_not change { existing.otp_last_sent_at } + end + end + + context 'match not found' do + it 'creates new record with otp_send_count = 0 and otp_last_sent_at = current time' do + expect { OtpRequestsTracker.find_or_create_with_phone(phone) }. + to change(OtpRequestsTracker, :count).by(1) + + existing = OtpRequestsTracker.where(phone_fingerprint: phone_fingerprint).first + + expect(existing.otp_send_count).to eq 0 + expect(existing.otp_last_sent_at).to be_within(2.seconds).of(Time.zone.now) + end + end + + context 'race condition' do + it 'retries once, then raises ActiveRecord::RecordNotUnique' do + tracker = OtpRequestsTracker.new + allow(OtpRequestsTracker).to receive(:where). + and_raise(ActiveRecord::RecordNotUnique.new(tracker)) + + expect(OtpRequestsTracker).to receive(:where).exactly(:once) + expect { OtpRequestsTracker.find_or_create_with_phone(phone) }. + to raise_error ActiveRecord::RecordNotUnique + end + end + end +end diff --git a/spec/presenters/two_factor_auth_code/phone_delivery_presenter_spec.rb b/spec/presenters/two_factor_auth_code/phone_delivery_presenter_spec.rb index b264973f0be..2a9aa55a07e 100644 --- a/spec/presenters/two_factor_auth_code/phone_delivery_presenter_spec.rb +++ b/spec/presenters/two_factor_auth_code/phone_delivery_presenter_spec.rb @@ -1,11 +1,95 @@ require 'rails_helper' describe TwoFactorAuthCode::PhoneDeliveryPresenter do - let(:presenter) { TwoFactorAuthCode::PhoneDeliveryPresenter.new({}) } + let(:data) do + { + code_value: '123abc', + totp_enabled: false, + phone_number: '***-***-5000', + unconfirmed_phone: false, + otp_delivery_preference: 'sms', + } + end + let(:view) { ActionController::Base.new.view_context } + let(:presenter) { TwoFactorAuthCode::PhoneDeliveryPresenter.new(data: data, view: view) } it 'is a subclass of GenericDeliveryPresenter' do expect(TwoFactorAuthCode::PhoneDeliveryPresenter.superclass).to( be(TwoFactorAuthCode::GenericDeliveryPresenter) ) end + + describe '#fallback_links' do + context 'with totp enabled' do + before do + data[:totp_enabled] = true + end + + context 'voice otp delivery supported' do + it 'renders an auth app fallback link' do + expect(presenter.fallback_links.join(' ')).to include( + I18n.t('links.two_factor_authentication.app') + ) + end + + it 'renders a voice otp link' do + expect(presenter.fallback_links.join(' ')).to include( + I18n.t('links.two_factor_authentication.voice') + ) + end + end + + context 'voice otp deliver unsupported' do + before do + data[:voice_otp_delivery_unsupported] = true + end + + it 'renders an auth app fallback link' do + expect(presenter.fallback_links.join(' ')).to include( + I18n.t('links.two_factor_authentication.app') + ) + end + + it 'does not render a voice otp link' do + expect(presenter.fallback_links.join(' ')).to_not include( + I18n.t('links.two_factor_authentication.voice') + ) + end + end + end + + context 'without totp enabled' do + context 'voice otp delivery supported' do + it 'does not render an auth app fallback link' do + expect(presenter.fallback_links.join(' ')).to_not include( + I18n.t('links.two_factor_authentication.app') + ) + end + + it 'renders a voice otp link' do + expect(presenter.fallback_links.join(' ')).to include( + I18n.t('links.two_factor_authentication.voice') + ) + end + end + + context 'voice otp deliver unsupported' do + before do + data[:voice_otp_delivery_unsupported] = true + end + + it 'does not render an auth app fallback link' do + expect(presenter.fallback_links.join(' ')).to_not include( + I18n.t('links.two_factor_authentication.app') + ) + end + + it 'does not render a voice otp link' do + expect(presenter.fallback_links.join(' ')).to_not include( + I18n.t('links.two_factor_authentication.voice') + ) + end + end + end + end end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 1ef54850cd0..5c106cb0494 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -51,6 +51,12 @@ FakeSms.messages = [] FakeVoiceCall.calls = [] end + + config.before(:each, idv_job: true) do + allow(VendorValidatorJob).to receive(:perform_later) do |*args| + VendorValidatorJob.perform_now(*args) + end + end end Sidekiq::Testing.inline! diff --git a/spec/services/idv/financials_step_spec.rb b/spec/services/idv/financials_step_spec.rb index 4e2407e82cb..1445ab394f7 100644 --- a/spec/services/idv/financials_step_spec.rb +++ b/spec/services/idv/financials_step_spec.rb @@ -7,58 +7,49 @@ idvs.vendor = :mock idvs end - let(:idv_finance_form) { Idv::FinanceForm.new(idv_session.params) } + let(:idv_form_params) { idv_session.params } - def build_step(params) + def build_step(vendor_validator_result) described_class.new( - idv_form: idv_finance_form, + idv_form_params: idv_form_params, idv_session: idv_session, - params: params + vendor_validator_result: vendor_validator_result ) end describe '#submit' do - it 'returns FormResponse with success: false for invalid params' do - step = build_step(finance_type: :ccn, ccn: '1234') - errors = { ccn: [t('idv.errors.invalid_ccn')] } - - response = instance_double(FormResponse) - allow(FormResponse).to receive(:new).and_return(response) - submission = step.submit - - expect(submission).to eq response - expect(FormResponse).to have_received(:new). - with(success: false, errors: errors) - expect(idv_session.financials_confirmation).to eq false - end - it 'returns FormResponse with success: true for mock-happy CCN' do - step = build_step(finance_type: :ccn, ccn: '12345678') + step = build_step( + Idv::VendorResult.new( + success: true, + errors: {} + ) + ) + + result = step.submit + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(true) + expect(result.errors).to be_empty - response = instance_double(FormResponse) - allow(FormResponse).to receive(:new).and_return(response) - - submission = step.submit - - expect(submission).to eq response - expect(FormResponse).to have_received(:new). - with(success: true, errors: {}) expect(idv_session.financials_confirmation).to eq true - expect(idv_session.params).to eq idv_finance_form.idv_params + expect(idv_session.params).to eq idv_form_params end it 'returns FormResponse with success: false for mock-sad CCN' do - step = build_step(finance_type: :ccn, ccn: '00000000') - errors = { ccn: ['The ccn could not be verified.'] } - response = instance_double(FormResponse) - allow(FormResponse).to receive(:new).and_return(response) - submission = step.submit + step = build_step( + Idv::VendorResult.new( + success: false, + errors: errors + ) + ) + + result = step.submit + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(false) + expect(result.errors).to eq(errors) - expect(submission).to eq response - expect(FormResponse).to have_received(:new). - with(success: false, errors: errors) expect(idv_session.financials_confirmation).to eq false end end diff --git a/spec/services/idv/financials_validator_spec.rb b/spec/services/idv/financials_validator_spec.rb index 0292c6b204d..2010f5bd8bb 100644 --- a/spec/services/idv/financials_validator_spec.rb +++ b/spec/services/idv/financials_validator_spec.rb @@ -3,13 +3,9 @@ describe Idv::FinancialsValidator do let(:user) { build(:user) } - let(:idv_session) do - idvs = Idv::Session.new(user_session: {}, current_user: user, issuer: nil) - idvs.vendor = :mock - idvs - end - - let(:session_id) { idv_session.vendor_session_id } + let(:applicant) { Proofer::Applicant.new({}) } + let(:vendor) { :mock } + let(:vendor_session_id) { SecureRandom.uuid } let(:params) do { ccn: '123-45-6789' } @@ -17,38 +13,42 @@ let(:confirmation) { instance_double(Proofer::Confirmation) } - subject { Idv::FinancialsValidator.new(idv_session: idv_session, vendor_params: params) } + subject do + Idv::FinancialsValidator.new( + applicant: applicant, + vendor: vendor, + vendor_params: params, + vendor_session_id: vendor_session_id + ) + end def stub_agent_calls agent = instance_double(Idv::Agent) allow(Idv::Agent).to receive(:new). - with(applicant: idv_session.applicant, vendor: :mock). + with(applicant: applicant, vendor: vendor). and_return(agent) expect(agent).to receive(:submit_financials). - with(params, idv_session.vendor_session_id).and_return(confirmation) + with(params, vendor_session_id).and_return(confirmation) end - describe '#success?' do - it 'returns Proofer::Confirmation#success?' do + describe '#result' do + it 'has success' do stub_agent_calls success_string = 'true' - expect(confirmation).to receive(:success?).and_return(success_string) - expect(subject.success?).to eq success_string + expect(subject.result.success?).to eq success_string end - end - describe '#error' do - it 'returns Proofer::Confirmation#errors' do + it 'has errors' do stub_agent_calls error_string = 'mucho errors' expect(confirmation).to receive(:errors).and_return(error_string) - expect(subject.errors).to eq error_string + expect(subject.result.errors).to eq error_string end end end diff --git a/spec/services/idv/phone_step_spec.rb b/spec/services/idv/phone_step_spec.rb index 3a609f56829..2ecb520ade6 100644 --- a/spec/services/idv/phone_step_spec.rb +++ b/spec/services/idv/phone_step_spec.rb @@ -10,53 +10,77 @@ idvs.applicant = Proofer::Applicant.new first_name: 'Some' idvs end + let(:idv_form_params) { { phone: '555-555-0000', phone_confirmed_at: nil } } let(:idv_phone_form) { Idv::PhoneForm.new(idv_session.params, user) } - def build_step(params) + def build_step(vendor_validator_result) described_class.new( - idv_form: idv_phone_form, idv_session: idv_session, - params: params + idv_form_params: idv_form_params, + vendor_validator_result: vendor_validator_result ) end describe '#submit' do - it 'returns false for invalid-looking phone' do - step = build_step(phone: '555') - - errors = { phone: [invalid_phone_message] } + it 'returns true for mock-happy phone' do + step = build_step( + Idv::VendorResult.new( + success: true, + errors: {} + ) + ) - result = instance_double(FormResponse) + result = step.submit - expect(FormResponse).to receive(:new). - with(success: false, errors: errors).and_return(result) - expect(step.submit).to eq result - expect(idv_session.phone_confirmation).to eq false + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(true) + expect(result.errors).to be_empty + expect(idv_session.vendor_phone_confirmation).to eq true + expect(idv_session.params).to eq idv_phone_form.idv_params end - it 'returns true for mock-happy phone' do - step = build_step(phone: '555-555-0000') + it 'returns false for mock-sad phone' do + idv_form_params[:phone] = '555-555-5555' + errors = { phone: ['The phone number could not be verified.'] } - result = instance_double(FormResponse) + step = build_step( + Idv::VendorResult.new( + success: false, + errors: errors + ) + ) - expect(FormResponse).to receive(:new).with(success: true, errors: {}). - and_return(result) - expect(step.submit).to eq result - expect(idv_session.phone_confirmation).to eq true - expect(idv_session.params).to eq idv_phone_form.idv_params + result = step.submit + + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(false) + expect(result.errors).to eq(errors) + expect(idv_session.vendor_phone_confirmation).to eq false end - it 'returns false for mock-sad phone' do - step = build_step(phone: '555-555-5555') + it 'marks the phone number as confirmed by user if it matches 2FA phone' do + idv_form_params[:phone_confirmed_at] = Time.zone.now + step = build_step( + Idv::VendorResult.new( + success: true, + errors: {} + ) + ) + step.submit - errors = { phone: ['The phone number could not be verified.'] } + expect(idv_session.user_phone_confirmation).to eq(true) + end - result = instance_double(FormResponse) + it 'does not mark the phone number as confirmed by user if it does not match 2FA phone' do + step = build_step( + Idv::VendorResult.new( + success: true, + errors: {} + ) + ) + step.submit - expect(FormResponse).to receive(:new). - with(success: false, errors: errors).and_return(result) - expect(step.submit).to eq result - expect(idv_session.phone_confirmation).to eq false + expect(idv_session.user_phone_confirmation).to eq(false) end end end diff --git a/spec/services/idv/phone_validator_spec.rb b/spec/services/idv/phone_validator_spec.rb index 83b849ca116..faa6755c778 100644 --- a/spec/services/idv/phone_validator_spec.rb +++ b/spec/services/idv/phone_validator_spec.rb @@ -3,13 +3,9 @@ describe Idv::PhoneValidator do let(:user) { build(:user) } - let(:idv_session) do - idvs = Idv::Session.new(user_session: {}, current_user: user, issuer: nil) - idvs.vendor = :mock - idvs - end - - let(:session_id) { idv_session.vendor_session_id } + let(:applicant) { Proofer::Applicant.new({}) } + let(:vendor) { :mock } + let(:vendor_session_id) { SecureRandom.uuid } let(:params) do { phone: '202-555-1212' } @@ -17,38 +13,43 @@ let(:confirmation) { instance_double(Proofer::Confirmation) } - subject { Idv::PhoneValidator.new(idv_session: idv_session, vendor_params: params) } + subject do + Idv::PhoneValidator.new( + applicant: applicant, + vendor: vendor, + vendor_params: params, + vendor_session_id: vendor_session_id + ) + end def stub_agent_calls agent = instance_double(Idv::Agent) allow(Idv::Agent).to receive(:new). - with(applicant: idv_session.applicant, vendor: :mock). + with(applicant: applicant, vendor: vendor). and_return(agent) expect(agent).to receive(:submit_phone). - with(params, idv_session.vendor_session_id).and_return(confirmation) + with(params, vendor_session_id).and_return(confirmation) end - describe '#success?' do - it 'returns Proofer::Confirmation#success?' do + describe '#result' do + it 'has success' do stub_agent_calls success_string = 'true' expect(confirmation).to receive(:success?).and_return(success_string) - expect(subject.success?).to eq success_string + expect(subject.result.success?).to eq success_string end - end - describe '#error' do - it 'returns Proofer::Confirmation#errors' do + it 'has errors' do stub_agent_calls error_string = 'mucho errors' expect(confirmation).to receive(:errors).and_return(error_string) - expect(subject.errors).to eq error_string + expect(subject.result.errors).to eq error_string end end end diff --git a/spec/services/idv/profile_step_spec.rb b/spec/services/idv/profile_step_spec.rb index e51f6f48c25..cc7655d0599 100644 --- a/spec/services/idv/profile_step_spec.rb +++ b/spec/services/idv/profile_step_spec.rb @@ -18,127 +18,139 @@ } end - def build_step(params) + def build_step(params, vendor_validator_result) + idv_session.params.merge!(params) + idv_session.applicant = idv_session.vendor_params + described_class.new( - idv_form: idv_profile_form, - idv_session: idv_session, - params: params + idv_form_params: params, + vendor_validator_result: vendor_validator_result, + idv_session: idv_session ) end describe '#submit' do it 'succeeds with good params' do - step = build_step(user_attrs) - - result = instance_double(FormResponse) + reasons = ['Everything looks good'] extra = { idv_attempts_exceeded: false, - vendor: { reasons: ['Everything looks good'] }, + vendor: { reasons: reasons }, } - expect(FormResponse).to receive(:new). - with(success: true, errors: {}, extra: extra).and_return(result) - expect(step.submit).to eq result + step = build_step( + user_attrs, + Idv::VendorResult.new( + success: true, + errors: {}, + reasons: reasons, + normalized_applicant: Proofer::Applicant.new(first_name: 'Some') + ) + ) + + result = step.submit + + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(true) + expect(result.errors).to be_empty + expect(result.extra).to eq(extra) expect(idv_session.profile_confirmation).to eq true end it 'fails with invalid SSN' do - step = build_step(user_attrs.merge(ssn: '666-66-6666')) - + reasons = ['The SSN was suspicious'] errors = { ssn: ['Unverified SSN.'] } extra = { idv_attempts_exceeded: false, - vendor: { reasons: ['The SSN was suspicious'] }, + vendor: { reasons: reasons }, } - result = instance_double(FormResponse) + step = build_step( + user_attrs.merge(ssn: '666-66-6666'), + Idv::VendorResult.new(success: false, errors: errors, reasons: reasons) + ) - expect(FormResponse).to receive(:new). - with(success: false, errors: errors, extra: extra).and_return(result) - expect(step.submit).to eq result - expect(idv_session.profile_confirmation).to be_nil - end - - it 'fails when form validation fails' do - step = build_step(user_attrs.merge(ssn: '6666')) + result = step.submit - errors = { ssn: [t('idv.errors.pattern_mismatch.ssn')] } - extra = { - idv_attempts_exceeded: false, - vendor: { reasons: nil }, - } - - result = instance_double(FormResponse) - - expect(FormResponse).to receive(:new). - with(success: false, errors: errors, extra: extra).and_return(result) - expect(step.submit).to eq result + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(false) + expect(result.errors).to eq(errors) + expect(result.extra).to eq(extra) expect(idv_session.profile_confirmation).to be_nil end it 'fails with invalid first name' do - step = build_step(user_attrs.merge(first_name: 'Bad')) - errors = { first_name: ['Unverified first name.'] } - - result = instance_double(FormResponse) + reasons = ['The name was suspicious'] extra = { idv_attempts_exceeded: false, - vendor: { reasons: ['The name was suspicious'] }, + vendor: { reasons: reasons }, } - expect(FormResponse).to receive(:new). - with(success: false, errors: errors, extra: extra).and_return(result) - expect(step.submit).to eq result + step = build_step( + user_attrs.merge(first_name: 'Bad'), + Idv::VendorResult.new(success: false, errors: errors, reasons: reasons) + ) + + result = step.submit + + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(false) + expect(result.errors).to eq(errors) + expect(result.extra).to eq(extra) expect(idv_session.profile_confirmation).to be_nil end it 'fails with invalid ZIP code on current address' do - step = build_step(user_attrs.merge(zipcode: '00000')) - + reasons = ['The ZIP code was suspicious'] errors = { zipcode: ['Unverified ZIP code.'] } - - result = instance_double(FormResponse) extra = { idv_attempts_exceeded: false, - vendor: { reasons: ['The ZIP code was suspicious'] }, + vendor: { reasons: reasons }, } - expect(FormResponse).to receive(:new). - with(success: false, errors: errors, extra: extra).and_return(result) - expect(step.submit).to eq result + step = build_step( + user_attrs.merge(zipcode: '00000'), + Idv::VendorResult.new(success: false, errors: errors, reasons: reasons) + ) + + result = step.submit + + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(false) + expect(result.errors).to eq(errors) + expect(result.extra).to eq(extra) expect(idv_session.profile_confirmation).to be_nil end it 'fails with invalid ZIP code on previous address' do - step = build_step(user_attrs.merge(prev_zipcode: '00000')) - + reasons = ['The ZIP code was suspicious'] errors = { zipcode: ['Unverified ZIP code.'] } - - result = instance_double(FormResponse) extra = { idv_attempts_exceeded: false, - vendor: { reasons: ['The ZIP code was suspicious'] }, + vendor: { reasons: reasons }, } - expect(FormResponse).to receive(:new). - with(success: false, errors: errors, extra: extra).and_return(result) - expect(step.submit).to eq result + step = build_step( + user_attrs.merge(prev_zipcode: '00000'), + Idv::VendorResult.new(success: false, errors: errors, reasons: reasons) + ) + + result = step.submit + + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(false) + expect(result.errors).to eq(errors) + expect(result.extra).to eq(extra) expect(idv_session.profile_confirmation).to be_nil end - it 'increments attempts count if the form is valid' do - step = build_step(user_attrs) + it 'increments attempts count' do + step = build_step(user_attrs, Idv::VendorResult.new(errors: {})) expect { step.submit }.to change(user, :idv_attempts).by(1) end - it 'does not increment the attempts count if the form is not valid' do - step = build_step(user_attrs.merge(ssn: '666')) - expect { step.submit }.to change(user, :idv_attempts).by(0) - end - it 'initializes the idv_session' do - step = build_step(user_attrs) + step = build_step(user_attrs, Idv::VendorResult.new(errors: {})) step.submit expect(idv_session.params).to eq user_attrs @@ -152,7 +164,7 @@ def build_step(params) allow(Idv::Attempter).to receive(:new).with(user).and_return(attempter) allow(attempter).to receive(:exceeded?) - step = build_step(user_attrs) + step = build_step(user_attrs, Idv::VendorResult.new(errors: {})) expect(step.attempts_exceeded?).to eq attempter.exceeded? end end diff --git a/spec/services/idv/session_spec.rb b/spec/services/idv/session_spec.rb index 7d0e6ccdea1..3e95c0cbbd5 100644 --- a/spec/services/idv/session_spec.rb +++ b/spec/services/idv/session_spec.rb @@ -33,4 +33,90 @@ end end end + + describe '#complete_session' do + context 'with phone verifed by vendor' do + before do + subject.address_verification_mechanism = :phone + subject.vendor_phone_confirmation = true + allow(subject).to receive(:complete_profile) + end + + it 'completes the profile if the user has completed OTP phone confirmation' do + subject.user_phone_confirmation = true + subject.complete_session + + expect(subject).to have_received(:complete_profile) + end + + it 'does not complete the profile if the user has not completed OTP phone confirmation' do + subject.user_phone_confirmation = nil + subject.complete_session + + expect(subject).not_to have_received(:complete_profile) + end + end + end + + describe '#phone_confirmed?' do + it 'returns true if the user and vendor have confirmed the phone' do + subject.user_phone_confirmation = true + subject.vendor_phone_confirmation = true + + expect(subject.phone_confirmed?).to eq(true) + end + + it 'returns false if the user has not confirmed the phone' do + subject.user_phone_confirmation = nil + subject.vendor_phone_confirmation = true + + expect(subject.phone_confirmed?).to eq(false) + end + + it 'returns false if the vendor has not confirmed the phone' do + subject.user_phone_confirmation = true + subject.vendor_phone_confirmation = nil + + expect(subject.phone_confirmed?).to eq(false) + end + + it 'returns false if neither the user nor the vendor has confirmed the phone' do + subject.user_phone_confirmation = nil + subject.vendor_phone_confirmation = nil + + expect(subject.phone_confirmed?).to eq(false) + end + end + + describe '#address_mechanism_chosen?' do + context 'phone verification chosen' do + before do + subject.address_verification_mechanism = 'phone' + end + + it 'returns true if the vendor has confirmed the phone number' do + subject.vendor_phone_confirmation = true + + expect(subject.address_mechanism_chosen?).to eq(true) + end + + it 'returns false if the vendor has not confirmed the phone number' do + subject.vendor_phone_confirmation = nil + + expect(subject.address_mechanism_chosen?).to eq(false) + end + end + + it 'returns true if the user has selected usps address verification' do + subject.address_verification_mechanism = 'usps' + + expect(subject.address_mechanism_chosen?).to eq(true) + end + + it 'returns false if the user has not selected phone or usps address verification' do + subject.address_verification_mechanism = nil + + expect(subject.address_mechanism_chosen?).to eq(false) + end + end end diff --git a/spec/services/idv/vendor_result_spec.rb b/spec/services/idv/vendor_result_spec.rb new file mode 100644 index 00000000000..74d39e2809f --- /dev/null +++ b/spec/services/idv/vendor_result_spec.rb @@ -0,0 +1,70 @@ +require 'rails_helper' + +RSpec.describe Idv::VendorResult do + let(:success) { true } + let(:errors) { { foo: ['is not valid'] } } + let(:reasons) { %w[foo bar baz] } + let(:session_id) { SecureRandom.uuid } + let(:normalized_applicant) do + Proofer::Applicant.new( + last_name: 'Ever', + first_name: 'Greatest' + ) + end + let(:timed_out) { false } + + subject(:vendor_result) do + Idv::VendorResult.new( + success: success, + errors: errors, + reasons: reasons, + session_id: session_id, + normalized_applicant: normalized_applicant, + timed_out: timed_out + ) + end + + describe '#success?' do + it 'is the success value' do + expect(vendor_result.success?).to eq(success) + end + end + + describe '#timed_out?' do + it 'is the timed_out value' do + expect(vendor_result.timed_out?).to eq(timed_out) + end + end + + describe '#to_json' do + it 'serializes normalized_applicant correctly' do + json = vendor_result.to_json + + parsed = JSON.parse(json, symbolize_names: true) + expect(parsed[:normalized_applicant][:last_name]).to eq(normalized_applicant.last_name) + end + end + + describe '.new_from_json' do + subject(:new_from_json) { Idv::VendorResult.new_from_json(vendor_result.to_json) } + + it 'has simple attributes' do + expect(new_from_json.success?).to eq(vendor_result.success?) + expect(new_from_json.errors).to eq(vendor_result.errors) + expect(new_from_json.reasons).to eq(vendor_result.reasons) + expect(new_from_json.session_id).to eq(vendor_result.session_id) + end + + it 'turns applicant into a full object' do + expect(new_from_json.normalized_applicant.last_name).to eq(normalized_applicant.last_name) + end + + context 'without an applicant' do + let(:normalized_applicant) { nil } + + it 'does not have an applicant' do + expect(new_from_json.normalized_applicant).to eq(nil) + end + end + end +end diff --git a/spec/services/marketing_site_spec.rb b/spec/services/marketing_site_spec.rb index bd72aca8384..80127eac095 100644 --- a/spec/services/marketing_site_spec.rb +++ b/spec/services/marketing_site_spec.rb @@ -3,7 +3,15 @@ RSpec.describe MarketingSite do describe '.base_url' do it 'points to the base URL' do - expect(MarketingSite.base_url).to eq('https://www.login.gov') + expect(MarketingSite.base_url).to eq('https://www.login.gov/') + end + + context 'when the user has set their locale to :es' do + before { I18n.locale = :es } + + it 'points to the base URL with the locale appended' do + expect(MarketingSite.base_url).to eq('https://www.login.gov/es/') + end end end @@ -11,18 +19,42 @@ it 'points to the privacy page' do expect(MarketingSite.privacy_url).to eq('https://www.login.gov/policy') end + + context 'when the user has set their locale to :es' do + before { I18n.locale = :es } + + it 'points to the privacy page with the locale appended' do + expect(MarketingSite.privacy_url).to eq('https://www.login.gov/es/policy') + end + end end describe '.contact_url' do it 'points to the contact page' do expect(MarketingSite.contact_url).to eq('https://www.login.gov/contact') end + + context 'when the user has set their locale to :es' do + before { I18n.locale = :es } + + it 'points to the contact page with the locale appended' do + expect(MarketingSite.contact_url).to eq('https://www.login.gov/es/contact') + end + end end describe '.help_url' do it 'points to the help page' do expect(MarketingSite.help_url).to eq('https://www.login.gov/help') end + + context 'when the user has set their locale to :es' do + before { I18n.locale = :es } + + it 'points to the help page with the locale appended' do + expect(MarketingSite.help_url).to eq('https://www.login.gov/es/help') + end + end end describe '.help_authenticator_app_url' do @@ -31,5 +63,15 @@ 'https://www.login.gov/help/signing-in/what-is-an-authenticator-app/' ) end + + context 'when the user has set their locale to :es' do + before { I18n.locale = :es } + + it 'points to the authenticator app section of the help page with the locale appended' do + expect(MarketingSite.help_authenticator_app_url).to eq( + 'https://www.login.gov/es/help/signing-in/what-is-an-authenticator-app/' + ) + end + end end end diff --git a/spec/services/otp_rate_limiter_spec.rb b/spec/services/otp_rate_limiter_spec.rb index bdf838dcac8..f2e6b2eb349 100644 --- a/spec/services/otp_rate_limiter_spec.rb +++ b/spec/services/otp_rate_limiter_spec.rb @@ -23,11 +23,13 @@ end describe '#increment' do - it 'sets the otp_last_sent_at' do - now = Time.zone.now + it 'updates otp_last_sent_at' do + tracker = OtpRequestsTracker.find_or_create_with_phone(current_user.phone) + old_otp_last_sent_at = tracker.reload.otp_last_sent_at otp_rate_limiter.increment + new_otp_last_sent_at = tracker.reload.otp_last_sent_at - expect(rate_limited_phone.otp_last_sent_at.to_i).to eq(now.to_i) + expect(new_otp_last_sent_at).to be > old_otp_last_sent_at end it 'increments the otp_send_count' do diff --git a/spec/services/phone_formatter_spec.rb b/spec/services/phone_formatter_spec.rb new file mode 100644 index 00000000000..8f32b271a2d --- /dev/null +++ b/spec/services/phone_formatter_spec.rb @@ -0,0 +1,45 @@ +require 'rails_helper' + +describe PhoneFormatter do + describe '#format' do + it 'formats international numbers correctly' do + phone = '+404004004000' + formatted_phone = PhoneFormatter.new.format(phone) + + expect(formatted_phone).to eq('+40 400 400 4000') + end + + it 'formats U.S. numbers correctly' do + phone = '+12025005000' + formatted_phone = PhoneFormatter.new.format(phone) + + expect(formatted_phone).to eq('+1 (202) 500-5000') + end + + it 'uses +1 as the default international code' do + phone = '2025005000' + formatted_phone = PhoneFormatter.new.format(phone) + + expect(formatted_phone).to eq('+1 (202) 500-5000') + end + + it 'uses the international code for the country specified in the country code option' do + phone = '123123123' + formatted_phone = PhoneFormatter.new.format(phone, country_code: 'MA') + + expect(formatted_phone).to eq('+212 12 3123 123') + end + + it 'returns nil for nil' do + formatted_phone = PhoneFormatter.new.format(nil) + + expect(formatted_phone).to be_nil + end + + it 'returns nil for nonsense' do + phone = '☎️📞📱📳' + formatted_phone = PhoneFormatter.new.format(phone) + expect(formatted_phone).to be_nil + end + end +end diff --git a/spec/services/phone_number_capabilities_spec.rb b/spec/services/phone_number_capabilities_spec.rb new file mode 100644 index 00000000000..ca994160040 --- /dev/null +++ b/spec/services/phone_number_capabilities_spec.rb @@ -0,0 +1,39 @@ +require 'rails_helper' + +describe PhoneNumberCapabilities do + let(:phone) { '+1 (555) 555-5000' } + subject { PhoneNumberCapabilities.new(phone) } + + describe '#sms_only?' do + context 'voice is supported' do + it { expect(subject.sms_only?).to eq(false) } + end + + context 'voice is not supported for the area code' do + let(:phone) { '+1 (671) 555-5000' } + it { expect(subject.sms_only?).to eq(true) } + end + + context 'voice is supported for the international code' do + let(:phone) { '+55 (555) 555-5000' } + it { expect(subject.sms_only?).to eq(false) } + end + + context 'voice is not supported for the international code' do + let(:phone) { '+212 1234 12345' } + it { expect(subject.sms_only?).to eq(true) } + end + end + + describe '#unsupported_location' do + it 'returns the name of the unsupported area code location' do + locality = PhoneNumberCapabilities.new('+1 (671) 555-5000').unsupported_location + expect(locality).to eq('Guam') + end + + it 'returns the name of the unsupported international code location' do + locality = PhoneNumberCapabilities.new('+212 1234 12345').unsupported_location + expect(locality).to eq('Morocco') + end + end +end diff --git a/spec/services/submit_idv_job_spec.rb b/spec/services/submit_idv_job_spec.rb new file mode 100644 index 00000000000..4c72132cc14 --- /dev/null +++ b/spec/services/submit_idv_job_spec.rb @@ -0,0 +1,58 @@ +require 'rails_helper' + +RSpec.describe SubmitIdvJob do + subject(:service) do + SubmitIdvJob.new( + vendor_validator_class: vendor_validator_class, + idv_session: idv_session, + vendor_params: vendor_params + ) + end + + let(:idv_session) do + Idv::Session.new( + current_user: user, + issuer: nil, + user_session: { + idv: { + applicant: applicant, + vendor_session_id: vendor_session_id, + vendor: :mock, + }, + } + ) + end + + let(:user) { build(:user) } + let(:applicant) { Proofer::Applicant.new(first_name: 'Greatest') } + let(:vendor_session_id) { '12345' } + let(:result_id) { 'abcdef' } + let(:vendor_params) { '+1 (888) 123-4567' } + let(:vendor_validator_class) { 'Idv::PhoneValidator' } + + describe '#call' do + subject(:call) { service.call } + + it 'generates a UUID and enqueues a job, and saves the UUID in the session' do + expect(SecureRandom).to receive(:uuid).and_return(result_id).once + + expect(VendorValidatorJob).to receive(:perform_later). + with( + result_id: result_id, + vendor_validator_class: vendor_validator_class, + vendor: 'mock', + vendor_params: vendor_params, + vendor_session_id: vendor_session_id, + applicant_json: applicant.to_json + ) + + expect(idv_session.async_result_id).to eq(nil) + expect(idv_session.async_result_started_at).to eq(nil) + + call + + expect(idv_session.async_result_id).to eq(result_id) + expect(idv_session.async_result_started_at).to be_within(1).of(Time.zone.now.to_i) + end + end +end diff --git a/spec/services/usps_exporter_spec.rb b/spec/services/usps_exporter_spec.rb index 2e4eba2144c..a2da4deee36 100644 --- a/spec/services/usps_exporter_spec.rb +++ b/spec/services/usps_exporter_spec.rb @@ -2,18 +2,17 @@ describe UspsExporter do let(:export_file) { Tempfile.new('usps_export.psv') } - let(:usps_entry) { UspsConfirmationEntry.new_from_hash(pii_attributes) } let(:pii_attributes) do - { - first_name: 'Some', - last_name: 'One', - address1: '123 Any St', - address2: 'Ste 123', - city: 'Somewhere', - state: 'KS', - zipcode: '66666-1234', - otp: 123, - } + Pii::Attributes.new_from_hash( + first_name: { raw: 'Söme', norm: 'Some' }, + last_name: { raw: 'Öne', norm: 'One' }, + address1: { raw: '123 Añy St', norm: '123 Any St' }, + address2: { raw: 'Sté 123', norm: 'Ste 123' }, + city: { raw: 'Sömewhere', norm: 'Somewhere' }, + state: { raw: 'KS', norm: 'KS' }, + zipcode: { raw: '66666-1234', norm: '66666-1234' }, + otp: { raw: 123, norm: 123 } + ) end let(:service_provider) { ServiceProvider.from_issuer('http://localhost:3000') } let(:psv_row_contents) do @@ -23,13 +22,13 @@ due_date = due.strftime('%-B %-e') values = [ UspsExporter::CONTENT_ROW_ID, - usps_entry.first_name + ' ' + usps_entry.last_name, - usps_entry.address1, - usps_entry.address2, - usps_entry.city, - usps_entry.state, - usps_entry.zipcode, - usps_entry.otp, + pii_attributes.first_name.norm + ' ' + pii_attributes.last_name.norm, + pii_attributes.address1.norm, + pii_attributes.address2.norm, + pii_attributes.city.norm, + pii_attributes.state.norm, + pii_attributes.zipcode.norm, + pii_attributes.otp.norm, "#{current_date}, #{now.year}", "#{due_date}, #{due.year}", service_provider.friendly_name, diff --git a/spec/services/usps_uploader_spec.rb b/spec/services/usps_uploader_spec.rb new file mode 100644 index 00000000000..b5f48ef530c --- /dev/null +++ b/spec/services/usps_uploader_spec.rb @@ -0,0 +1,39 @@ +require 'rails_helper' + +RSpec.describe UspsUploader do + subject(:uploader) { UspsUploader.new } + + describe '#run' do + subject(:run) { uploader.run } + + let(:sftp_connection) { instance_double('Net::SFTP::Session') } + + before do + sftp_options = [ + Figaro.env.equifax_sftp_host, + Figaro.env.equifax_sftp_username, + { key_data: [RequestKeyManager.equifax_ssh_key.to_pem] }, + ] + expect(Net::SFTP).to receive(:start). + with(*sftp_options).and_yield(sftp_connection) + end + + it 'creates a PGP-encrypted file and uploads it via SFTP and deletes it after' do + expect(sftp_connection).to receive(:upload!). + with(uploader.local_path.to_s, File.join(Figaro.env.equifax_sftp_directory, 'batch.pgp')) + + run + + expect(File.exist?(uploader.local_path)).to eq(false) + end + + it 'notifies NewRelic and does not delete the file if SFTP fails' do + expect(sftp_connection).to receive(:upload!).and_raise(StandardError) + expect(NewRelic::Agent).to receive(:notice_error) + + expect { run }.to_not raise_error + + expect(File.exist?(uploader.local_path)).to eq(true) + end + end +end diff --git a/spec/services/vendor_validator_result_storage_spec.rb b/spec/services/vendor_validator_result_storage_spec.rb new file mode 100644 index 00000000000..6cd36a9270f --- /dev/null +++ b/spec/services/vendor_validator_result_storage_spec.rb @@ -0,0 +1,51 @@ +require 'rails_helper' + +RSpec.describe VendorValidatorResultStorage do + subject(:service) { VendorValidatorResultStorage.new } + + let(:session_id) { SecureRandom.uuid } + let(:result_id) { SecureRandom.uuid } + let(:original_result) do + Idv::VendorResult.new( + success: false, + session_id: session_id, + normalized_applicant: Proofer::Applicant.new(first_name: 'First') + ) + end + + describe '#store_result' do + it 'stores the result in redis with a TTL' do + key = service.redis_key(result_id) + + before_redis = Sidekiq.redis { |redis| redis.get(key) } + expect(before_redis).to be_nil + + service.store(result_id: result_id, result: original_result) + + Sidekiq.redis do |redis| + expect(redis.get(key)).to be_present + expect(redis.ttl(key)).to be_within(1).of(VendorValidatorResultStorage::TTL) + end + end + end + + describe '#vendor_validator_result' do + before { service.store(result_id: result_id, result: original_result) } + + it 'retrieves a stored result' do + result = service.load(result_id) + + expect(result.success?).to eq(original_result.success?) + expect(result.errors).to eq(original_result.errors) + expect(result.reasons).to eq(original_result.reasons) + expect(result.normalized_applicant.as_json). + to eq(original_result.normalized_applicant.as_json) + end + + it 'is nil with a bad result id' do + result = service.load(SecureRandom.uuid) + + expect(result).to be_nil + end + end +end diff --git a/spec/support/features/session_helper.rb b/spec/support/features/session_helper.rb index f5831cf230b..63da64d2b21 100644 --- a/spec/support/features/session_helper.rb +++ b/spec/support/features/session_helper.rb @@ -24,8 +24,8 @@ def signin(email, password) end def fill_in_credentials_and_submit(email, password) - fill_in 'Email', with: email - fill_in 'Password', with: password + fill_in 'user_email', with: email + fill_in 'user_password', with: password click_button t('links.next') end diff --git a/spec/support/shared_examples_for_otp_delivery_preference_validation.rb b/spec/support/shared_examples_for_otp_delivery_preference_validation.rb new file mode 100644 index 00000000000..ad2964464f6 --- /dev/null +++ b/spec/support/shared_examples_for_otp_delivery_preference_validation.rb @@ -0,0 +1,45 @@ +shared_examples 'an otp delivery preference form' do + let(:phone) { '+1 (555) 555-5000' } + let(:params) do + { + phone: phone, + otp_delivery_preference: 'voice', + international_code: 'US', + } + end + + context 'voice' do + it 'is valid when supported for the phone' do + update_user = instance_double(UpdateUser) + attributes = { otp_delivery_preference: 'voice' } + allow(UpdateUser).to receive(:new).with(user: user, attributes: attributes). + and_return(update_user) + expect(update_user).to receive(:call) + + capabilities = spy(PhoneNumberCapabilities) + allow(PhoneNumberCapabilities).to receive(:new).with(phone).and_return(capabilities) + allow(capabilities).to receive(:sms_only?).and_return(false) + + result = subject.submit(params) + + expect(result.success?).to eq(true) + end + + it 'is invalid when unsupported for the phone' do + update_user = instance_double(UpdateUser) + attributes = { otp_delivery_preference: 'voice' } + allow(UpdateUser).to receive(:new).with(user: user, attributes: attributes). + and_return(update_user) + expect(update_user).to_not receive(:call) + + capabilities = spy(PhoneNumberCapabilities) + allow(PhoneNumberCapabilities).to receive(:new).with(phone).and_return(capabilities) + allow(capabilities).to receive(:sms_only?).and_return(true) + + result = subject.submit(params) + + expect(result.success?).to eq(false) + expect(result.errors).to include(:phone) + end + end +end diff --git a/spec/support/shared_examples_for_phone_validation.rb b/spec/support/shared_examples_for_phone_validation.rb index a6527b2831b..d05b00379f1 100644 --- a/spec/support/shared_examples_for_phone_validation.rb +++ b/spec/support/shared_examples_for_phone_validation.rb @@ -1,4 +1,8 @@ +require 'shoulda/matchers' + shared_examples 'a phone form' do + include Shoulda::Matchers::ActiveModel + describe 'phone presence validation' do it 'is invalid when phone is blank' do subject.submit(phone: '') @@ -8,12 +12,26 @@ end describe 'phone validation' do - it 'uses the phony_rails gem with country option set to US' do + it 'uses the phony_rails gem' do phone_validator = subject._validators.values.flatten. detect { |v| v.class == PhonyPlausibleValidator } - expect(phone_validator.options). - to eq(country_code: 'US', presence: true, message: :improbable_phone) + expect(phone_validator.options[:presence]).to eq(true) + expect(phone_validator.options[:message]).to eq(:improbable_phone) + expect(phone_validator.options).to include(:international_code) + end + + it do + should validate_inclusion_of(:international_code). + in_array(PhoneNumberCapabilities::INTERNATIONAL_CODES.keys) + end + + it 'validates that the number matches the requested international code' do + result = subject.submit(phone: '123 123 1234', international_code: 'MA') + + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(false) + expect(result.errors).to include(:phone) end end @@ -24,26 +42,32 @@ allow(User).to receive(:exists?).with(email: 'new@gmail.com').and_return(false) allow(User).to receive(:exists?).with(phone: second_user.phone).and_return(true) - expect(subject.submit(phone: second_user.phone)).to be true + result = subject.submit(phone: second_user.phone, international_code: 'US') + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(true) end end context 'when phone is not already taken' do it 'is valid' do - expect(subject.submit(phone: '+1 (703) 555-1212')).to be true + result = subject.submit(phone: '+1 (703) 555-1212', international_code: 'US') + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to be true end end context 'when phone is same as current user' do it 'is valid' do - expect(subject.submit(phone: user.phone)).to be true + result = subject.submit(phone: user.phone, international_code: 'US') + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to be true end end end describe '#submit' do it 'formats the phone before assigning it' do - subject.submit(phone: '703-555-1212') + subject.submit(phone: '703-555-1212', international_code: 'US') expect(subject.phone).to eq '+1 (703) 555-1212' end diff --git a/spec/view_models/verify/base_spec.rb b/spec/view_models/verify/base_spec.rb index c36daff9791..a5dcd367dc8 100644 --- a/spec/view_models/verify/base_spec.rb +++ b/spec/view_models/verify/base_spec.rb @@ -22,4 +22,36 @@ end end end + + describe '#message' do + let(:timed_out) { false } + let(:view_model) do + Verify::Base.new( + error: error, + remaining_attempts: 1, + idv_form: nil, + timed_out: timed_out + ) + end + + subject(:message) { view_model.message } + + before { expect(view_model).to receive(:step_name).and_return(:phone) } + + context 'with a warning' do + let(:error) { 'warning' } + + it 'uses the warning copy' do + expect(message).to include(t('idv.modal.phone.warning')) + end + + context 'with a timeout' do + let(:timed_out) { true } + + it 'uses the timeout copy' do + expect(message).to include(t('idv.modal.phone.timeout')) + end + end + end + end end diff --git a/spec/views/devise/mailer/confirmation_instructions.html.slim_spec.rb b/spec/views/devise/mailer/confirmation_instructions.html.slim_spec.rb index 5d27b671432..b5900800675 100644 --- a/spec/views/devise/mailer/confirmation_instructions.html.slim_spec.rb +++ b/spec/views/devise/mailer/confirmation_instructions.html.slim_spec.rb @@ -26,6 +26,21 @@ ) end + context 'in a non-default locale' do + before { assign(:locale, 'fr') } + + it 'puts the locale in the URL' do + assign(:resource, build_stubbed(:user, confirmed_at: Time.zone.now)) + assign(:token, 'foo') + render + + expect(rendered).to have_link( + 'http://test.host/fr/sign_up/email/confirm?confirmation_token=foo', + href: 'http://test.host/fr/sign_up/email/confirm?confirmation_token=foo' + ) + end + end + it 'mentions updating an account when user has already been confirmed' do user = build_stubbed(:user, confirmed_at: Time.zone.now) presenter = ConfirmationEmailPresenter.new(user, self) diff --git a/spec/views/devise/passwords/new.html.slim_spec.rb b/spec/views/devise/passwords/new.html.slim_spec.rb index 5cee8700ed1..d6669688339 100644 --- a/spec/views/devise/passwords/new.html.slim_spec.rb +++ b/spec/views/devise/passwords/new.html.slim_spec.rb @@ -1,8 +1,6 @@ require 'rails_helper' describe 'devise/passwords/new.html.slim' do - let(:user) { build_stubbed(:user) } - before do @password_reset_email_form = PasswordResetEmailForm.new('') sp = build_stubbed( @@ -17,7 +15,6 @@ sp_session: {}, service_provider_request: ServiceProviderRequest.new ).call - allow(view).to receive(:current_user).and_return(user) allow(view).to receive(:decorated_session).and_return(@decorated_session) end @@ -38,4 +35,10 @@ expect(rendered).to have_xpath("//form[@autocomplete='off']") end + + it 'has a cancel link that points to the decorated_session cancel_link_path' do + render + + expect(rendered).to have_link(t('links.cancel'), href: @decorated_session.cancel_link_path) + end end diff --git a/spec/views/layouts/application.html.slim_spec.rb b/spec/views/layouts/application.html.slim_spec.rb index ed2129717fa..dd18a895ea4 100644 --- a/spec/views/layouts/application.html.slim_spec.rb +++ b/spec/views/layouts/application.html.slim_spec.rb @@ -15,6 +15,8 @@ ) allow(view.request).to receive(:original_url).and_return('http://test.host/foobar') allow(view).to receive(:current_user).and_return(User.new) + controller.request.path_parameters[:controller] = 'users/sessions' + controller.request.path_parameters[:action] = 'new' end context 'no content for nav present' do diff --git a/spec/views/shared/_footer_lite.html.slim_spec.rb b/spec/views/shared/_footer_lite.html.slim_spec.rb index 567f63c4a45..78196788915 100644 --- a/spec/views/shared/_footer_lite.html.slim_spec.rb +++ b/spec/views/shared/_footer_lite.html.slim_spec.rb @@ -2,6 +2,11 @@ describe 'shared/_footer_lite.html.slim' do context 'user is signed out' do + before do + controller.request.path_parameters[:controller] = 'users/sessions' + controller.request.path_parameters[:action] = 'new' + end + it 'contains link to help page' do render diff --git a/spec/views/two_factor_authentication/otp_verification/show.html.slim_spec.rb b/spec/views/two_factor_authentication/otp_verification/show.html.slim_spec.rb index ff54370186c..5be1df804c5 100644 --- a/spec/views/two_factor_authentication/otp_verification/show.html.slim_spec.rb +++ b/spec/views/two_factor_authentication/otp_verification/show.html.slim_spec.rb @@ -44,28 +44,40 @@ expect(rendered).to have_selector("form[action='/users'][method='post']") end - it 'informs the user that an OTP has been sent to their number via #help_text' do - build_stubbed(:user) + context 'OTP copy' do + let(:help_text) do + code_link = link_to( + t('links.two_factor_authentication.resend_code.sms'), + otp_send_path( + otp_delivery_selection_form: { + otp_delivery_preference: 'sms', + resend: true, + } + ) + ) - code_link = link_to( - t('links.two_factor_authentication.resend_code.sms'), - otp_send_path( - otp_delivery_selection_form: { - otp_delivery_preference: 'sms', - resend: true, - } + t( + "instructions.mfa.#{presenter_data[:otp_delivery_preference]}.confirm_code_html", + number: "#{presenter_data[:phone_number]}", + resend_code_link: code_link ) - ) + end - help_text = t( - "instructions.2fa.#{presenter_data[:otp_delivery_preference]}.confirm_code_html", - number: "#{presenter_data[:phone_number]}", - resend_code_link: code_link - ) + it 'informs the user that an OTP has been sent to their number via #help_text' do + render - render + expect(rendered).to include help_text + end - expect(rendered).to include help_text + context 'in other locales' do + before { I18n.locale = :es } + + it 'translates correctly' do + render + + expect(rendered).to include help_text + end + end end context 'user signed up' do @@ -206,7 +218,7 @@ render expect(rendered).to include( - t("instructions.2fa.#{otp_delivery_preference}.fallback_html", link: expected_link) + t("instructions.mfa.#{otp_delivery_preference}.fallback_html", link: expected_link) ) end @@ -224,7 +236,7 @@ render expect(rendered).not_to include( - t('instructions.2fa.voice.fallback_html', link: unexpected_link) + t('instructions.mfa.voice.fallback_html', link: unexpected_link) ) end end @@ -270,7 +282,7 @@ render expect(rendered).to include( - t("instructions.2fa.#{otp_delivery_preference}.fallback_html", link: expected_link) + t("instructions.mfa.#{otp_delivery_preference}.fallback_html", link: expected_link) ) end @@ -288,7 +300,7 @@ render expect(rendered).not_to include( - t('instructions.2fa.sms.fallback_html', link: unexpected_link) + t('instructions.mfa.sms.fallback_html', link: unexpected_link) ) end end diff --git a/spec/views/verify/review/new.html.slim_spec.rb b/spec/views/verify/review/new.html.slim_spec.rb index 258c9d201da..3ffe21b10a1 100644 --- a/spec/views/verify/review/new.html.slim_spec.rb +++ b/spec/views/verify/review/new.html.slim_spec.rb @@ -48,7 +48,8 @@ end it 'renders a link telling user why financial info is not visible' do - expect(rendered).to have_link t('idv.messages.review.financial_info') + expect(rendered). + to have_link(t('idv.messages.review.financial_info'), href: MarketingSite.help_url) end end end diff --git a/spec/views/verify/usps/index.html.slim_spec.rb b/spec/views/verify/usps/index.html.slim_spec.rb new file mode 100644 index 00000000000..c72e0b65701 --- /dev/null +++ b/spec/views/verify/usps/index.html.slim_spec.rb @@ -0,0 +1,18 @@ +require 'rails_helper' + +describe 'verify/usps/index.html.slim' do + it 'calls UspsDecorator#title and #button' do + user = build_stubbed(:user, :signed_up) + usps_mail_service = Idv::UspsMail.new(user) + + usps_decorator = instance_double(UspsDecorator) + allow(UspsDecorator).to receive(:new).with(usps_mail_service). + and_return(usps_decorator) + @decorated_usps = usps_decorator + + expect(usps_decorator).to receive(:title) + expect(usps_decorator).to receive(:button) + + render + end +end