Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 6 additions & 19 deletions app/controllers/users/reset_passwords_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,12 @@ def request_id
end

def handle_valid_email
create_account_if_email_not_found
RequestPasswordReset.new(
email: email,
request_id: request_id,
analytics: analytics,
irs_attempts_api_tracker: irs_attempts_api_tracker,
).perform

session[:email] = email
resend_confirmation = email_params[:resend]
Expand All @@ -103,24 +108,6 @@ def store_token_in_session
session[:reset_password_token] = params[:reset_password_token]
end

def create_account_if_email_not_found
user, result = RequestPasswordReset.new(
email: email,
request_id: request_id,
analytics: analytics,
irs_attempts_api_tracker: irs_attempts_api_tracker,
).perform

return unless result

analytics.user_registration_email(**result.to_h)
irs_attempts_api_tracker.user_registration_email_submitted(
email: email,
success: result.success?,
)
create_user_event(:account_created, user)
end

def handle_invalid_or_expired_token(result)
flash[:error] = t("devise.passwords.#{result.errors[:user].first}")
session.delete(:reset_password_token)
Expand Down
24 changes: 7 additions & 17 deletions app/forms/register_user_email_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,13 @@ class RegisterUserEmailForm

attr_reader :email_address, :terms_accepted
attr_accessor :email_language
attr_accessor :password_reset_requested

def self.model_name
ActiveModel::Name.new(self, nil, 'User')
end

def initialize(analytics:, attempts_tracker:, password_reset_requested: false)
def initialize(analytics:, attempts_tracker:)
@rate_limited = false
@password_reset_requested = password_reset_requested
@analytics = analytics
@attempts_tracker = attempts_tracker
end
Expand Down Expand Up @@ -53,7 +51,7 @@ def validate_terms_accepted
errors.add(:terms_accepted, t('errors.registration.terms'), type: :terms)
end

def submit(params, instructions = nil)
def submit(params)
@terms_accepted = !!ActiveModel::Type::Boolean.new.cast(params[:terms_accepted])
build_user_and_email_address_with_email(
email: params[:email],
Expand All @@ -62,7 +60,7 @@ def submit(params, instructions = nil)
self.request_id = params[:request_id]

self.success = valid?
process_successful_submission(request_id, instructions) if success
process_successful_submission(request_id) if success

FormResponse.new(success: success, errors: errors, extra: extra_analytics_attributes)
end
Expand All @@ -72,10 +70,6 @@ def email_taken?
@email_taken = lookup_email_taken
end

def password_reset_requested?
@password_reset_requested
end

private

attr_writer :email, :email_address
Expand All @@ -98,7 +92,7 @@ def lookup_email_taken
true
end

def process_successful_submission(request_id, instructions)
def process_successful_submission(request_id)
# To prevent discovery of existing emails, we check to see if the email is
# already taken and if so, we act as if the user registration was successful.
if email_address_record&.user&.suspended?
Expand All @@ -111,7 +105,7 @@ def process_successful_submission(request_id, instructions)
elsif email_taken?
send_sign_up_confirmed_email
else
send_sign_up_email(request_id, instructions)
send_sign_up_email(request_id)
end
end

Expand Down Expand Up @@ -140,7 +134,7 @@ def rate_limit!(rate_limit_type)
@rate_limited = rate_limiter.limited?
end

def send_sign_up_email(request_id, instructions)
def send_sign_up_email(request_id)
rate_limit!(:reg_unconfirmed_email)

if rate_limited?
Expand All @@ -154,11 +148,7 @@ def send_sign_up_email(request_id, instructions)
user.accepted_terms_at = Time.zone.now
user.save!

SendSignUpEmailConfirmation.new(user).call(
request_id: email_request_id(request_id),
instructions: instructions,
password_reset_requested: password_reset_requested?,
)
SendSignUpEmailConfirmation.new(user).call(request_id: email_request_id(request_id))
end
end

Expand Down
54 changes: 54 additions & 0 deletions app/mailers/anonymous_mailer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# frozen_string_literal: true

# AnonymousMailer handles all email sending not associated with a user. It expects to be called
# using `with` that receives an `email` string value.
#
# You MUST deliver these messages using `deliver_now`. Anonymous messages rely on a plaintext email
# address, which is personally-identifiable information (PII). All method arguments are stored in
# the database when the email is being sent asynchronously by ActiveJob and we must not put PII in
# the database in plaintext.
#
# Example:
#
# AnonymousMailer.with(email:).password_reset_missing_user(request_id:).deliver_now
#
class AnonymousMailer < ActionMailer::Base
include Mailable
include LocaleHelper

before_action :attach_images

after_action :add_metadata

default(
from: email_with_name(
IdentityConfig.store.email_from,
IdentityConfig.store.email_from_display_name,
),
reply_to: email_with_name(
IdentityConfig.store.email_from,
IdentityConfig.store.email_from_display_name,
),
)

layout 'mailer'

def password_reset_missing_user(request_id:)
@request_id = request_id

mail(
to: email,
subject: t('anonymous_mailer.password_reset_missing_user.subject'),
)
end

private

def email
params.fetch(:email)
end

def add_metadata
message.instance_variable_set(:@_metadata, action: action_name)
end
end
23 changes: 5 additions & 18 deletions app/mailers/user_mailer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
#
# Arguments sent to UserMailer must not include personally-identifiable information (PII).
# This includes email addresses. All arguments to UserMailer are stored in the database when the
# email is being sent asynchronusly by ActiveJob and we must not put PII in the database in
# email is being sent asynchronously by ActiveJob and we must not put PII in the database in
# plaintext.
#
# Example:
Expand Down Expand Up @@ -38,6 +38,8 @@ class UserEmailAddressMismatchError < StandardError; end
),
)

layout 'mailer'

def validate_user_and_email_address
@user = params.fetch(:user)
@email_address = params.fetch(:email_address)
Expand All @@ -56,10 +58,10 @@ def add_metadata
)
end

def email_confirmation_instructions(token, request_id:, instructions:)
def email_confirmation_instructions(token, request_id:)
with_user_locale(user) do
presenter = ConfirmationEmailPresenter.new(user, view_context)
@first_sentence = instructions || presenter.first_sentence
@first_sentence = presenter.first_sentence
@confirmation_period = presenter.confirmation_period
@request_id = request_id
@locale = locale_url_param
Expand All @@ -71,21 +73,6 @@ def email_confirmation_instructions(token, request_id:, instructions:)
end
end

def unconfirmed_email_instructions(token, request_id:, instructions:)
with_user_locale(user) do
presenter = ConfirmationEmailPresenter.new(user, view_context)
@first_sentence = instructions || presenter.first_sentence
@confirmation_period = presenter.confirmation_period
@request_id = request_id
@locale = locale_url_param
@token = token
mail(
to: email_address.email,
subject: t('user_mailer.email_confirmation_instructions.email_not_found'),
)
end
end

def signup_with_your_email
with_user_locale(user) do
@root_url = root_url(locale: locale_url_param)
Expand Down
54 changes: 5 additions & 49 deletions app/services/request_password_reset.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,13 @@
allowed_members: [:request_id]
) do
def perform
if user_should_receive_registration_email?
form = RegisterUserEmailForm.new(
password_reset_requested: true,
analytics: analytics,
attempts_tracker: irs_attempts_api_tracker,
)
result = form.submit({ email: email, terms_accepted: '1' }, instructions)
[form.user, result]
else
send_reset_password_instructions
nil
end
end

private

def send_reset_password_instructions
rate_limiter = RateLimiter.new(user: user, rate_limit_type: :reset_password_email)
rate_limiter = RateLimiter.new(target: email, rate_limit_type: :reset_password_email)
rate_limiter.increment!
if rate_limiter.limited?
analytics.rate_limit_reached(limiter_type: :reset_password_email)
irs_attempts_api_tracker.forgot_password_email_rate_limited(email: email)
elsif user.blank?
AnonymousMailer.with(email:).password_reset_missing_user(request_id:).deliver_now
elsif user.suspended?
UserMailer.with(
user: user,
Expand All @@ -47,42 +32,13 @@ def send_reset_password_instructions
end
end

def instructions
I18n.t(
'user_mailer.email_confirmation_instructions.first_sentence.forgot_password',
app_name: APP_NAME,
)
end

##
# If a user record does not exist for an email address, we send a registration
# email instead of a reset email so the user can go through the account
# creation process without having to receive another email
#
# If a user exists but does not have any confirmed email addresses, we send
# them a reset email so they can set the password on the account
#
# If a user exists and has a confirmed email addresses, but this email address
# is not confirmed we should not let them reset the password with this email
# address. Instead we send them an email to create an account with the
# unconfirmed email address
##
def user_should_receive_registration_email?
return true if user.nil?
return false unless user.confirmed?
return false if email_address_record.confirmed?
true
end
private

def user
@user ||= email_address_record&.user
end

# We want to find the EmailAddress with preferring to find the confirmed one first
# if both a confirmed and an unconfirmed row exist
def email_address_record
@email_address_record ||= begin
EmailAddress.find_with_confirmed_or_unconfirmed_email(email)
end
@email_address_record ||= EmailAddress.confirmed.find_with_email(email)
end
end.freeze
20 changes: 3 additions & 17 deletions app/services/send_sign_up_email_confirmation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,9 @@ def initialize(user)
@user = user
end

def call(request_id: nil, instructions: nil, password_reset_requested: false)
def call(request_id: nil)
update_email_address_record

if password_reset_requested && !user.confirmed?
send_pw_reset_request_unconfirmed_user_email(request_id, instructions)
else
send_confirmation_email(request_id, instructions)
end
send_confirmation_email(request_id)
end

private
Expand Down Expand Up @@ -53,19 +48,10 @@ def update_email_address_record
)
end

def send_confirmation_email(request_id, instructions)
def send_confirmation_email(request_id)
UserMailer.with(user: user, email_address: email_address).email_confirmation_instructions(
confirmation_token,
request_id: request_id,
instructions: instructions,
).deliver_now_or_later
end

def send_pw_reset_request_unconfirmed_user_email(request_id, instructions)
UserMailer.with(user: user, email_address: email_address).unconfirmed_email_instructions(
confirmation_token,
request_id: request_id,
instructions: instructions,
).deliver_now_or_later
end

Expand Down
42 changes: 42 additions & 0 deletions app/views/anonymous_mailer/password_reset_missing_user.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<p class="lead" style="margin-bottom: 1em">
<%= t('anonymous_mailer.password_reset_missing_user.info_no_account', app_name: APP_NAME) %>
</p>
<p class="lead">
<%= t('anonymous_mailer.password_reset_missing_user.info_request_different', app_name: APP_NAME) %>
</p>

<table class="button expanded large radius">
<tbody>
<tr>
<td>
<table style="margin-bottom: 1em">
<tbody>
<tr>
<td style="text-align: center">
<%= link_to t('anonymous_mailer.password_reset_missing_user.try_different_email'),
new_user_password_url(request_id: @request_id),
target: '_blank',
class: 'float-center',
align: 'center',
rel: 'noopener' %>
</td>
</tr>
</tbody>
</table>
</td>
<td class="expander"></td>
</tr>
</tbody>
</table>

<p>
<%= t(
'anonymous_mailer.password_reset_missing_user.use_this_email_html',
create_account_link_html: link_to(
t('anonymous_mailer.password_reset_missing_user.create_new_account'),
sign_up_register_url(request_id: @request_id),
target: '_blank',
rel: 'noopener',
),
) %>
</p>
Loading