Email change flow with Rails 8 auth
Summary
With the Email verification in place, we build a flow that let's the user change his Email after verificationLesson
Picking up
This lesson builds on Email verification with Rails 8 auth: Part 1 and Email verification with Rails 8 auth: Part 2
Change an Email
For this interaction, the model EmailChange
fully comes to life, because it serves as an trace of a given User's Email addresses over time, and holds the value to which we need to update the field after successful (re)verification. One difference to the case with initial email verification is that this action needs to be triggered by the user. I'll illustrate the case where the user can go to a settings page, give his "new" address in a field and submit the form, there are of course many different places where this could be done instead. Let's add a route and controller
# config/routes.rb
#...
namespace :settings do
resources :emails, only: [ :new, :create, :edit ], param: :token
end
class Settings::EmailsController < ApplicationController
before_action :set_email_change_by_token, only: %i[ edit ]
def new
end
def create
return redirect_to new_settings_email_path, alert: "Please verify your email address first" unless Current.user.email_verified?
return redirect_to new_settings_email_path, alert: "Can't change to this email address" if User.exists? email_address: params[:email_address]
if EmailChange.where(user: Current.user, confirmed_at: nil, created_at: 1.day.ago..).exists?
redirect_to new_settings_email_path, alert: "Please wait a little longer before requesting another email change. If the issue persists, please contact support at support@example.com"
else
Current.user.initiate_email_change params[:email_address]
redirect_to new_settings_email_path, notice: "Email change instructions have been sent, please check your inbox for the new address."
end
end
def edit
if Current.user != @email_change.user
return redirect_to new_settings_email_path, alert: "You are not authorized to change this email."
end
if @email_change.confirmed_at?
return redirect_to new_settings_email_path, alert: "Email change has already been confirmed."
end
if Current.user.update email_address: @email_change.to
@email_change.update confirmed_at: Time.zone.now
redirect_to edit_settings_info_path, notice: "Email has been changed."
else
redirect_to edit_settings_info_path, alert: "Could not change email."
end
end
private
def set_email_change_by_token
@email_change = EmailChange.find_by_token_for!(:email_change, params[:token])
rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveRecord::RecordNotFound
redirect_to new_settings_email_path, alert: "Email change link is invalid or has expired."
end
end
The controller is a bit different from the EmailVerificationsController
:
- we provide a new
action to show a form to input the new address
- in the edit
action, we don't change the email_verified
field because the user needs to have his initial mail verified before making a change.
- we changing the email_address
field instead
We need an additional method in User
def initiate_email_change(new_email)
email_change = EmailChange.create(user: self, from: self.email_address, to: new_email)
token = email_change.generate_token_for :email_change
NotificationMailer.email_change(email_change, token).deliver_later
end
The new
view can be as simple as this
<h1>Change your email</h1>
<%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %>
<%= form_with url: settings_emails_path do |form| %>
<%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your new email address", value: params[:email_address] %><br>
<%= form.submit "Change Email" %>
<% end %>
Remember to also add a different mailer action email_change
that has maybe some different copy.
With this, we can safely change a user's email, and have a trail of changes in the associated EmailChange
model!