- Generate User model.
rails g model User email:string
# db/migrate/[timestamp]_create_users.rb
class CreateUsers < ActiveRecord::Migration[6.1]
def change
create_table :users do |t|
t.string :email, null: false
t.timestamps
end
add_index :users, :email, unique: true
end
end
- Run migrations.
rails db:migrate
- Add validations and callbacks.
# app/models/user.rb
class User < ApplicationRecord
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
before_save :downcase_email
validates :email, format: { with: VALID_EMAIL_REGEX }, presence: true, uniqueness: true
private
def downcase_email
self.email = self.email.downcase
end
end
What's Going On Here?
- We prevent empty values from being saved into the email column through a
null: false
constraint in addition to the presence validation.- We enforce unique email addresses at the database level through
add_index :users, :email, unique: true
in addition to a uniqueness validation.- We ensure all emails are valid through a format validation.
- We save all emails to the database in a downcase format via a before_save callback such that the values are saved in a consistent format.
- Create migration.
rails g migration add_confirmation_and_password_columns_to_users confirmation_token:string confirmation_sent_at:datetime confirmed_at:datetime password_digest:string
- Update the migration.
# db/migrate/[timestamp]_add_confirmation_and_password_columns_to_users.rb
class AddConfirmationAndPasswordColumnsToUsers < ActiveRecord::Migration[6.1]
def change
add_column :users, :confirmation_token, :string, null: false
add_column :users, :confirmation_sent_at, :datetime
add_column :users, :confirmed_at, :datetime
add_column :users, :password_digest, :string, null: false
add_index :users, :confirmation_token, unique: true
end
end
What's Going On Here?
- The
confirmation_token
column will store a random value created through the has_secure_token method when a record is saved. This will be used to identify users in a secure way when we need to confirm their email address. We addnull: false
to prevent empty values and also add a unique index to ensure that no two users will have the sameconfirmation_token
. You can think of this as a secure alternative to theid
column.- The
confirmation_sent_at
column will be used to ensure a confirmation has not expired. This is an added layer of security to prevent aconfirmation_token
from being used multiple times.- The
confirmed_at
column will be set when a user confirms their account. This will help us determine who has confirmed their account and who has not.- The
password_digest
column will store a hashed version of the user's password. This is provided by the has_secure_password( method.
- Run migrations.
rails db:migrate
- Enable and install BCrypt.
This is needed to to use has_secure_password
.
# Gemfile
gem 'bcrypt', '~> 3.1.7'
bundle install
- Update the User Model.
# app/models/user.rb
class User < ApplicationRecord
CONFIRMATION_TOKEN_EXPIRATION_IN_SECONDS = 10.minutes.to_i
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
has_secure_password
has_secure_token :confirmation_token
before_save :downcase_email
validates :email, format: { with: VALID_EMAIL_REGEX }, presence: true, uniqueness: true
def confirm!
self.update_columns(confirmed_at: Time.current)
end
def confirmed?
self.confirmed_at.present?
end
def confirmation_token_has_not_expired?
return false if self.confirmation_sent_at.nil?
(Time.current - self.confirmation_sent_at) <= User::CONFIRMATION_TOKEN_EXPIRATION_IN_SECONDS
end
def unconfirmed?
self.confirmed_at.nil?
end
private
def downcase_email
self.email = self.email.downcase
end
end
What's Going On Here?
- The
has_secure_password
method is added to give us an API to work with thepassword_digest
column.- The
has_secure_token :confirmation_token
method is added to give us an API to work with theconfirmation_token
column.- The
confirm!
method will be called when a user confirms their email address. We still need to build this feature.- The
confirmed?
andunconfirmed?
methods allow us to tell whether a user has confirmed their email address or not.- The
confirmation_token_has_not_expired?
method tells us if the confirmation token is expired or not. This can be controlled by changing the value of theCONFIRMATION_TOKEN_EXPIRATION_IN_SECONDS
constant. This will be useful when we build the confirmation mailer.
- Create a simple home page since we'll need a place to redirect users to after they sign up.
rails g controller StaticPages home
- Create UsersController.
rails g controller Users
# app/controllers/users_controller.rb
class UsersController < ApplicationController
def create
@user = User.new(user_params)
if @user.save
redirect_to root_path, notice: "Please check your email for confirmation instructions."
else
render :new
end
end
def new
@user = User.new
end
private
def user_params
params.require(:user).permit(:email, :password, :password_confirmation)
end
end
- Build sign up form.
<!-- app/views/shared/_form_errors.html.erb -->
<% if object.errors.any? %>
<ul>
<% object.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
<% end %>
<!-- app/views/users/new.html.erb -->
<%= form_with model: @user, url: sign_up_path do |form| %>
<%= render partial: "shared/form_errors", locals: { object: form.object } %>
<div>
<%= form.label :email %>
<%= form.text_field :email, required: true %>
</div>
<div>
<%= form.label :password %>
<%= form.password_field :password, required: true %>
</div>
<div>
<%= form.label :password_confirmation %>
<%= form.password_field :password_confirmation, required: true %>
</div>
<%= form.submit %>
<% end %>
- Update routes.
# config/routes.rb
Rails.application.routes.draw do
root "static_pages#home"
post "sign_up", to: "users#create"
get "sign_up", to: "users#new"
end
Users now have a way to sign up, but we need to verify their email address in order to prevent SPAM.
- Create ConfirmationsController
rails g controller Confirmations
# app/controllers/confirmations_controller.rb
class ConfirmationsController < ApplicationController
def create
@user = User.find_by(email: params[:user][:email].downcase)
if @user && @user.unconfirmed?
redirect_to root_path, notice: "Check your email for confirmation instructions."
else
redirect_to new_confirmation_path, alert: "We could not find a user with that email or that email has already been confirmed."
end
end
def edit
@user = User.find_by(confirmation_token: params[:confirmation_token])
if @user && @user.confirmation_token_has_not_expired?
@user.confirm!
redirect_to root_path, notice: "Your account has been confirmed."
else
redirect_to new_confirmation_path, alert: "Invalid token."
end
end
def new
@user = User.new
end
end
- Build confirmation pages.
This page will be used in the case where a user did not receive their confirmation instructions and needs to have them resent.
<!-- app/views/confirmations/new.html.erb -->
<%= form_with model: @user, url: confirmations_path do |form| %>
<%= form.email_field :email, required: true %>
<%= form.submit "Confirm Email" %>
<% end %>
- Update routes.
# config/routes.rb
Rails.application.routes.draw do
...
resources :confirmations, only: [:create, :edit, :new], param: :confirmation_token
end
What's Going On Here?
- The
create
action will be used to resend confirmation instructions to a user who is unconfirmed. We still need to build this mailer, and we still need to send this mailer when a user initially signs up. This action will be requested via the form onapp/views/confirmations/new.html.erb
. Note that we calldowncase
on the email to account for case sensitivity when searching.- The
edit
action is used to confirm a user's email. This will be the page that a user lands on when they click the confirmation link in their email. We still need to build this. Note that we're looking up a user through theirconfirmation_token
and not their email or ID. This is because Theconfirmation_token
is randomly generated and can't be easily guessed unlike an email or numeric ID. This is also why we addedparam: :confirmation_token
as a named route parameter. Note that we check if their confirmation token has expired before confirming their account.
Now we need a way to send a confirmation email to our users in order for them to actually confirm their accounts.
- Create confirmation mailer.
rails g mailer User confirmation
# app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer
default from: User::MAILER_FROM_EMAIL
def confirmation(user)
@user = user
mail to: @user.email, subject: "Confirmation Instructions"
end
end
<!-- app/views/user_mailer/confirmation.html.erb -->
<h1>Confirmation Instructions</h1>
<%= link_to "Click here to confirm your email.", edit_confirmation_url(@user.confirmation_token) %>
<!-- app/views/user_mailer/confirmation.text.erb -->
Confirmation Instructions
<%= edit_confirmation_url(@user.confirmation_token) %>
- Update User Model.
# app/models/user.rb
class User < ApplicationRecord
...
MAILER_FROM_EMAIL = "no-reply@example.com"
...
def send_confirmation_email!
self.regenerate_confirmation_token
self.update_columns(confirmation_sent_at: Time.current)
UserMailer.confirmation(self).deliver_now
end
end
What's Going On Here?
- The
MAILER_FROM_EMAIL
constant is a way for us to set the email used in theUserMailer
. This is optional.- The
send_confirmation_email!
method will create a newconfirmation_token
and update the value ofconfirmation_sent_at
. This is to ensure confirmation links expire and cannot be reused. It will also send a the confirmation email to the user.- We call update_columns so that the
updated_at/updated_on
columns are not updated. This is personal preference, but those columns should typically only be updated when the user updates their email or password.- The links in the mailer will take the user to
ConfirmationsController#edit
at which point they'll be confirmed.
- Configure Action Mailer so that links work locally.
Add a host to the test and development (and later the production) environments so that urls will work in mailers.
# config/environments/test.rb
Rails.application.configure do
...
config.action_mailer.default_url_options = { host: "example.com" }
end
# config/environments/development.rb
Rails.application.configure do
...
config.action_mailer.default_url_options = { host: "localhost", port: 3000 }
end
- Update Controllers.
Now we can send a confirmation email when a user signs up or if they need to have it resent.
# app/controllers/confirmations_controller.rb
class ConfirmationsController < ApplicationController
def create
@user = User.find_by(email: params[:user][:email].downcase)
if @user && @user.unconfirmed?
@user.send_confirmation_email!
...
end
end
end
# app/controllers/users_controller.rb
class UsersController < ApplicationController
def create
@user = User.new(user_params)
if @user.save
@user.send_confirmation_email!
...
end
end
end
- Create a model to store the current user.
# app/models/current.rb
class Current < ActiveSupport::CurrentAttributes
attribute :user
end
- Create a Concern to store helper methods that will be shared accross the application.
# app/controllers/concerns/authentication.rb
module Authentication
extend ActiveSupport::Concern
included do
before_action :current_user
helper_method :current_user
helper_method :user_signed_in?
end
def login(user)
reset_session
session[:current_user_id] = user.id
end
def logout
reset_session
end
def redirect_if_authenticated
redirect_to root_path, alert: "You are already logged in." if user_signed_in?
end
private
def current_user
Current.user = session[:current_user_id] && User.find_by(id: session[:current_user_id])
end
def user_signed_in?
Current.user.present?
end
end
- Load the Authentication Concern into the Application Controller.
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
include Authentication
end
What's Going On Here?
- The
Current
class inherits from ActiveSupport::CurrentAttributes which allows us to keep all per-request attributes easily available to the whole system. In essence this will allow us to set a current user and have access to that user during each request to the server.- The
Authentication
Concern provides an interface for logging the user in and out. We load it into theApplicationController
so that it will be used acrosss the whole application.
- The
login
method first resets the session to account for session fixation.- We set the user's ID in the session so that we can have access to the user across requests. The user's ID won't be stored in plain text. The cookie data is cryptographically signed to make it tamper-proof. And it is also encrypted so anyone with access to it can't read its contents.
- The
logout
method simply resets the session.- The
redirect_if_authenticated
method checks to see if the user is logged in. If they are, they'll be redirected to theroot_path
. This will be useful on pages an authenticated user should not be able to access, such as the login page.- The
current_user
method returns aUser
and sets it as the user on theCurrent
class we created. We call thebefore_action
filter so that we have access to the current user before each request. We also add this as a helper_method so that we have access tocurrent_user
in the views.- The
user_signed_in?
method simply returns true or false depending on whether the user is signed in or not. This is helpful for conditionally rendering items in views.
- Generate Sessions Controller.
rails g controller Sessions
# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
before_action :redirect_if_authenticated, only: [:create, :new]
def create
@user = User.find_by(email: params[:user][:email].downcase)
if @user
if @user.unconfirmed?
redirect_to new_confirmation_path, alert: "You must confirm your email before you can sign in."
elsif @user.authenticate(params[:user][:password])
login @user
redirect_to root_path, notice: "Signed in."
else
flash[:alert] = "Incorrect email or password."
render :new
end
else
flash[:alert] = "Incorrect email or password."
render :new
end
end
def destroy
logout
redirect_to root_path, notice: "Singed out."
end
def new
end
end
- Update routes.
# config/routes.rb
Rails.application.routes.draw do
...
post "login", to: "sessions#create"
delete "logout", to: "sessions#destroy"
get "login", to: "sessions#new"
end
- Add sign in form.
<!-- app/views/sessions/new.html.erb -->
<%= form_with url: login_path, scope: :user do |form| %>
<div>
<%= form.label :email %>
<%= form.text_field :email, required: true %>
</div>
<div>
<%= form.label :password %>
<%= form.password_field :password, required: true %>
</div>
<div>
<%= form.label :password_confirmation %>
<%= form.password_field :password_confirmation, required: true %>
</div>
<%= form.submit %>
<% end %>
What's Going On Here?
- The
create
method simply simply checks if the user exists and is confirmed. If they are, then we check their password. If the password is correct, we log them in via thelogin
method we created in theAuthentication
Concern. Otherwise, we render a an alert.
- We're able to call
user.authenticate
because of has_secure_password- Note that we call
downcase
on the email to account for case sensitivity when searching.- The
destroy
method simply calls thelogout
method we created in theAuthentication
Concern.- The login form is passed a
scope: :user
option so that the params are namespaced asparams[:user][:some_value]
. This is not required, but it helps keep things organized.
- Updated Controllers to prevent authenticated users from accessing pages intended for anonymous users.
# app/controllers/confirmations_controller.rb
class ConfirmationsController < ApplicationController
before_action :redirect_if_authenticated, only: [:create, :new]
def edit
...
if @user && @user.confirmation_token_has_not_expired?
@user.confirm!
login @user
...
else
end
...
end
end
Note that we also call login @user
once a user is confirmed. That way they'll be automatically logged in after confirming their email.
# app/controllers/users_controller.rb
class UsersController < ApplicationController
before_action :redirect_if_authenticated, only: [:create, :new]
...
end
- Create migration.
rails g migration add_password_reset_token_to_users password_reset_token:string password_reset_sent_at:datetime
- Update the migration.
# db/migrate/[timestamp]_add_password_reset_token_to_users.rb
class AddPasswordResetTokenToUsers < ActiveRecord::Migration[6.1]
def change
add_column :users, :password_reset_token, :string, null: false
add_column :users, :password_reset_sent_at, :datetime
add_index :users, :password_reset_token, unique: true
end
end
What's Going On Here?
- The
password_reset_token
column will store a random value created through the has_secure_token method when a record is saved. This will be used to identify users in a secure way when they need to reset their password. We addnull: false
to prevent empty values and also add a unique index to ensure that no two users will have the samepassword_reset_token
. You can think of this as a secure alternative to theid
column.- The
password_reset_sent_at
column will be used to ensure a password reset link has not expired. This is an added layer of security to prevent apassword_reset_token
from being used multiple times.
- Run migration.
rails db:migrate
- Update User Model.
# app/models/user.rb
class User < ApplicationRecord
...
PASSWORD_RESET_TOKEN_EXPIRATION_IN_SECONDS = 10.minutes.to_i
...
has_secure_token :password_reset_token
...
def password_reset_token_has_expired?
return true if self.password_reset_sent_at.nil?
(Time.current - self.password_reset_sent_at) >= User::PASSWORD_RESET_TOKEN_EXPIRATION_IN_SECONDS
end
def send_password_reset_email!
self.regenerate_password_reset_token
self.update_columns(password_reset_sent_at: Time.current)
UserMailer.password_reset(self).deliver_now
end
...
end
- Update User Mailer.
# app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer
...
def password_reset(user)
@user = user
mail to: @user.email, subject: "Password Reset Instructions"
end
end
What's Going On Here?
- The
has_secure_token :password_reset_token
method is added to give us an API to work with thepassword_reset_token
column.- The
password_reset_token_has_expired?
method tells us if the password reset token is expired or not. This can be controlled by changing the value of thePASSWORD_RESET_TOKEN_EXPIRATION_IN_SECONDS
constant. This will be useful when we build the password reset mailer.- The
send_password_reset_email!
method will create a newpassword_reset_token
and update the value ofpassword_reset_sent_at
. This is to ensure password reset links expire and cannot be reused. It will also send a the password reset email to the user. We still need to build this.
- Create PasswordsController.
rails g controller Passwords
# app/controllers/passwords_controller.rb
class PasswordsController < ApplicationController
before_action :redirect_if_authenticated
def create
@user = User.find_by(email: params[:user][:email].downcase)
if @user.present?
if @user.confirmed?
@user.send_password_reset_email!
redirect_to root_path, notice: "If that user exists we've sent instructions to their email."
else
redirect_to new_confirmation_path, alert: "Please confirm your email first."
end
else
redirect_to root_path, notice: "If that user exists we've sent instructions to their email."
end
end
def edit
@user = User.find_by(password_reset_token: params[:password_reset_token])
if @user && @user.unconfirmed?
redirect_to new_confirmation_path, alert: "You must confirm your email before you can sign in."
elsif @user.nil? || @user.password_reset_token_has_expired?
redirect_to new_password_path, alert: "Invalid or expired token."
end
end
def new
end
def update
@user = User.find_by(password_reset_token: params[:password_reset_token])
if @user
if @user.unconfirmed?
redirect_to new_confirmation_path, alert: "You must confirm your email before you can sign in."
elsif @user.password_reset_token_has_expired?
redirect_to new_password_path, alert: "Incorrect email or password."
elsif @user.update(password_params)
redirect_to login_path, notice: "Signed in."
else
flash[:alert] = @user.errors.full_messages.to_sentence
render :edit
end
else
flash[:alert] = "Incorrect email or password."
render :new
end
end
private
def password_params
params.require(:user).permit(:password, :password_confirmation)
end
end
What's Going On Here?
- The
create
action will send an email to the user containing a link that will allow them to reset the password. The link will contain theirpassword_reset_token
which is unique and expires. Note that we calldowncase
on the email to account for case sensitivity when searching.
- Note that we return
If that user exists we've sent instructions to their email.
even if the user is not found. This makes it difficult for a bad actor to use the reset form to see which email accounts exist on the application.- The
edit
action renders simply renders the form for the user to update their password. It attempts to find a user by therepassword_reset_token
. You can think of thepassword_reset_token
as a way to identify the user much like how we normally identify records by their ID. However, thepassword_reset_token
is randomly generated and will expire so it's more secure.- The
new
action simply renders a form for the user to put their email address in to receive the password reset email.- The
update
also ensures the user is identified by theirpassword_reset_token
. It's not enough to just do this on theedit
action since a bad actor could make aPUT
request to the server and bypass the form.
- If the user exists and is confirmed and their password token has not expired, we update their password to the one they will set in the form. Otherwise we handle each failure case a little different.
- Update Routes.
# config/routes.rb
Rails.application.routes.draw do
...
resources :passwords, only: [:create, :edit, :new, :update], param: :password_reset_token
end
What's Going On Here?
- We add
param: :password_reset_token
as a named route parameter to the so that we can identify users by theirpassword_reset_token
and notid
. This is similar to what we did with the confirmations routes, and ensures a user cannot be identified by their ID.
- Build forms.
<!-- app/views/passwords/new.html.erb -->
<%= form_with url: passwords_path, scope: :user do |form| %>
<%= form.email_field :email, required: true %>
<%= form.submit "Reset Password" %>
<% end %>
<!-- app/views/passwords/edit.html.erb -->
<%= form_with url: password_path(@user.password_reset_token), scope: :user, method: :put do |form| %>
<div>
<%= form.label :password %>
<%= form.password_field :password, required: true %>
</div>
<div>
<%= form.label :password_confirmation %>
<%= form.password_field :password_confirmation, required: true %>
</div>
<%= form.submit "Update Password" %>
<% end %>
What's Going On Here?
- The password reset form is passed a
scope: :user
option so that the params are namespaced asparams[:user][:some_value]
. This is not required, but it helps keep things organized.
- Create migration and run migration
rails g migration add_unconfirmed_email_to_users unconfirmed_email:string
rails db:migrate
- Update User Model.
# app/models/user.rb
class User < ApplicationRecord
...
attr_accessor :current_password
...
before_save :downcase_unconfirmed_email
...
validates :unconfirmed_email, format: { with: VALID_EMAIL_REGEX, allow_blank: true }
validate :unconfirmed_email_must_be_available
def confirm!
if self.unconfirmed_email.present?
self.update(email: self.unconfirmed_email, unconfirmed_email: nil)
end
self.update_columns(confirmed_at: Time.current)
end
...
def confirmable_email
if self.unconfirmed_email.present?
self.unconfirmed_email
else
self.email
end
end
...
def reconfirming?
self.unconfirmed_email.present?
end
def unconfirmed_or_reconfirming?
self.unconfirmed? || self.reconfirming?
end
private
...
def downcase_unconfirmed_email
return if self.unconfirmed_email.nil?
self.unconfirmed_email = self.unconfirmed_email.downcase
end
def unconfirmed_email_must_be_available
return if self.unconfirmed_email.nil?
if User.find_by(email: self.unconfirmed_email.downcase)
errors.add(:unconfirmed_email, "is already in use.")
end
end
end
- Update User Mailer.
# app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer
def confirmation(user)
...
mail to: @user.confirmable_email, subject: "Confirmation Instructions"
end
end
What's Going On Here?
- We add a
unconfirmed_email
to theusers_table
so that we have a place to store the email a user is trying to use after their account has been confirmed with their original email.- We add
attr_accessor :current_password
so that we'll be able to usef.password_field :current_password
in the user form (which doesn't exist yet). This will allow us to require the user to submit their current password before they can update their account.- We ensure to format the
unconfirmed_email
before saving to the database. This ensures all data is saved consistently.- We add validations to the
unconfirmed_email
column ensuring it's a valid email address and that it's not currently in use.- We update the
confirm!
method to set theunconfirmed_email
column, and then clear out theunconfirmed_email
column. This will only happen if a user is trying to confirm a new email address.- We add the
confirmable_email
method so that we can call the correct email in the the updatedUserMailer
.- We add
reconfirming?
andunconfirmed_or_reconfirming?
to help us determine what state a user is in. This will come in handy later in our controllers.
- Update Authentication Concern
# app/controllers/concerns/authentication.rb
module Authentication
...
def authenticate_user!
redirect_to login_path, alert: "You need to login to access that page." unless user_signed_in?
end
...
end
What's Going On Here?
- The
authenticate_user!
method can be called to ensure an anonymous user cannot access a page that requires a user to be logged in. We'll need this when we build the page allowing a user to edit or delete their profile.
- Add destroy, edit and update methods. Modify create method and user_params.
# app/controllers/users_controller.rb
class UsersController < ApplicationController
before_action :authenticate_user!, only: [:edit, :destroy, :update]
...
def create
@user = User.new(create_user_params)
...
end
def destroy
current_user.destroy
reset_session
redirect_to root_path, notice: "Your account has been deleted."
end
def edit
@user = current_user
end
...
def update
@user = current_user
if @user.authenticate(params[:user][:current_password])
if @user.update(update_user_params)
if params[:user][:unconfirmed_email].present?
@user.send_confirmation_email!
redirect_to root_path, notice: "Check your email for confirmation instructions."
else
redirect_to root_path, notice: "Account updated."
end
else
render :edit, status: :unprocessable_entity
end
else
flash.now[:error] = "Incorrect password"
render :edit, status: :unprocessable_entity
end
end
private
def create_user_params
params.require(:user).permit(:email, :password, :password_confirmation)
end
def update_user_params
params.require(:user).permit(:current_password, :password, :password_confirmation, :unconfirmed_email)
end
end
What's Going On Here?
- We call
redirect_if_authenticated
before editing, destroying or updating a user, since only an authenticated use should be able to do this.- We update the
create
method to acceptcreate_user_params
(formerlyuser_params
). This is because we're going to require different parameters for creating an account vs. editing an account.- The
destroy
action simply deletes the user and logs them out. Note that we're callingcurrent_user
, so this action can only be scoped to the user who is logged in.- The
edit
action simply assigns@user
to thecurrent_user
so that we have access to the user in the edit form.- The
update
action first checks if their password is correct. Note that we're passing this in ascurrent_password
and notpassword
. This is because we still want a user to be able to change their password and therefor we need another parameter to store this value. This is also why we have a privateupdate_user_params
method.
- If the user is updating their email address (via
unconfirmed_email
) we send a confirmation email to that new email address before setting it as the- We force a user to always put in their
current_password
as an extra security measure incase someone leaves their browser open on a public computer.
- Update routes.
# config/routes.rb
Rails.application.routes.draw do
...
put "account", to: "users#update"
get "account", to: "users#edit"
delete "account", to: "users#destroy"
...
end
- Create edit form.
<!-- app/views/users/edit.html.erb -->
<%= form_with model: @user, url: account_path, method: :put do |form| %>
<%= render partial: "shared/form_errors", locals: { object: form.object } %>
<div>
<%= form.label :email, "Current Email" %>
<%= form.text_field :email, disabled: true %>
</div>
<div>
<%= form.label :unconfirmed_email, "New Email" %>
<%= form.text_field :unconfirmed_email %>
</div>
<div>
<%= form.label :password, "Password (leave blank if you don't want to change it)" %>
<%= form.password_field :password %>
</div>
<div>
<%= form.label :password_confirmation %>
<%= form.password_field :password_confirmation %>
</div>
<hr/>
<div>
<%= form.label :current_password, "Current password (we need your current password to confirm your changes)" %>
<%= form.password_field :current_password, required: true %>
</div>
<%= form.submit %>
<% end %>
What's Going On Here?
- We
disable
the- We
require
thecurrent_password
field since we'll always want to a user to confirm their password before making changes.- The
password
andpassword_confirmation
fields are there if a user wants to update their current password.
- Update edit action.
# app/controllers/confirmations_controller.rb
class ConfirmationsController < ApplicationController
...
def edit
...
if @user && @user.unconfirmed_or_reconfirming? && @user.confirmation_token_has_not_expired?
...
end
end
...
end
What's Going On Here?
- We add
@user.unconfirmed_or_reconfirming?
to the conditional to ensure only unconfirmed users or users who are reconfirming can access this page. This is necessary since we're now allowing users to confirm new email addresses.