Chapter 22

Users

Clients are now getting authenticated with every request and without a valid API key, their request won’t go through. We can now shift our attention to users. We want people to sign up on Alexandria before they order their ebooks.

Identifying, authenticating and authorizing users will be done in the next chapter. For now, we need to create the User model and its associated controller. After that, we will add a way for users to confirm their accounts and reset their passwords.

We will be doing everything from scratch instead of using a gem like Devise or Authlogic. While I do not recommend rolling your own authentication every time you create a new application, my experience with the authentication gems for API-only applications has been disappointing. That’s why I’ve decided to show you how to build it from scratch so you understand the difference with the authentication system in a regular Rails application.

22.1. The User Model

The User model will have quite a lot of fields. We need to not only store user’s personal information, but also some metadata that will help the admin. We also need some columns to store tokens for the “confirmation” and “reset password” processes.

Here is the list of columns for the users table.

  • id
  • email
  • password_digest
  • given_name
  • family_name
  • last_logged_in_at
  • confirmation_token
  • confirmation_redirect_link
  • confirmed_at
  • confirmation_sent_at
  • reset_password_token
  • reset_password_redirect_link
  • reset_password_sent_at

To secure the password, we will be using the bcrypt gem associated with the has_secure_password method.

Add the bcrypt gem to your Gemfile

# Gemfile
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby '2.5.0'

gem 'rails', '5.2.0'
gem 'pg'
gem 'puma', '~> 3.11'
gem 'bootsnap', '>= 1.1.0', require: false
gem 'carrierwave'
gem 'carrierwave-base64'
gem 'pg_search'
gem 'kaminari'
gem 'bcrypt', '~> 3.1.7'

# Hidden Code

and get it installed.

bundle install

Now, let’s generate the User model.

rails g model User email:string password_digest:string given_name:string \
  family_name:string last_logged_in_at:timestamp confirmation_token:string \
  confirmed_at:timestamp confirmation_sent_at:timestamp \
  reset_password_token:string reset_password_redirect_url:text \
  reset_password_sent_at:timestamp role:integer

Output

Running via Spring preloader in process 36599
      invoke  active_record
      create    db/migrate/TIMESTAMP_create_users.rb
      create    app/models/user.rb
      invoke    rspec
      create      spec/models/user_spec.rb
      invoke      factory_bot
      create        spec/factories/users.rb

We need to add indexes and unique constraints to some of the columns in the users table. More specifically, we need indexes on all the columns that we will use to search for users: email for authentication, confirmation_token for confirmation, and reset_password_token for the “reset password” process.

# db/migrate/TIMESTAMP_create_users.rb
class CreateUsers < ActiveRecord::Migration[5.2]
  def change
    create_table :users do |t|
      t.string :email, index: true, unique: true
      t.string :password_digest
      t.string :given_name
      t.string :family_name
      t.timestamp :last_logged_in_at
      t.string :confirmation_token, index: true, unique: true
      t.text :confirmation_redirect_url
      t.timestamp :confirmed_at
      t.timestamp :confirmation_sent_at
      t.string :reset_password_token, index: true, unique: true
      t.text :reset_password_redirect_url
      t.timestamp :reset_password_sent_at
      t.integer :role, default: 0

      t.timestamps
    end
  end
end

Run the migration we just generated.

rails db:migrate && RAILS_ENV=test rails db:migrate

Let’s update the factory for the User model.

# spec/factories/users.rb
FactoryBot.define do
  factory :user do
    email { 'john@example.com' }
    password { 'password' }
    given_name { 'John' }
    family_name { 'Doe' }
    role { :user }

    trait :confirmation_redirect_url do
      confirmation_token { '123' }
      confirmation_redirect_url { 'http://google.com' }
    end

    trait :confirmation_no_redirect_url do
      confirmation_token { '123' }
      confirmation_redirect_url { nil }
    end

    trait :reset_password do
      reset_password_token { '123' }
      reset_password_redirect_url { 'http://example.com?some=params' }
      reset_password_sent_at { Time.now }
    end

    trait :reset_password_no_params do
      reset_password_token { '123' }
      reset_password_redirect_url { 'http://example.com' }
      reset_password_sent_at { Time.now }
    end

  end
end

Now we can define the expectations for the behavior of the User model.

# spec/models/user_spec.rb
require 'rails_helper'

RSpec.describe User, :type => :model do
  let(:user) { build(:user) }

  it 'has a valid factory' do
   expect(build(:user)).to be_valid
  end

  it { should validate_presence_of(:email) }
  it { should validate_uniqueness_of(:email).ignoring_case_sensitivity }
  it { should validate_presence_of(:password) }

  it 'generates a confirmation token' do
    user.valid?
    expect(user.confirmation_token).to_not be nil
  end

  it 'downcases email before validating' do
    user.email = 'John@example.com'
    expect(user.valid?).to be true
    expect(user.email).to eq 'john@example.com'
  end

end

And here is the User model. It’s pretty standard, but pay attention to how we generate the confirmation token when a user is created and how we downcase the email before the validation step.

# app/models/user.rb
class User < ApplicationRecord
  has_secure_password

  before_validation :generate_confirmation_token, on: :create
  before_validation :downcase_email

  enum role: [:user, :admin]

  validates :email, presence: true,
                uniqueness: true,
                length: { maximum: 255 },
                format: { with: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i }

  validates :password, presence: true, length: { minimum: 8 }, if: :new_record?
  validates :given_name, length: { maximum: 100 }
  validates :family_name, length: { maximum: 100 }
  validates :confirmation_token, presence: true,
                                 uniqueness: { case_sensitive: true }

  private

  def generate_confirmation_token
    self.confirmation_token = SecureRandom.hex
  end

  def downcase_email
    email.downcase! if email
  end

end

Run the tests.

rspec spec/models/user_spec.rb

Success (GREEN)

User
  has a valid factory
  should validate that :email cannot be empty/falsy
  should validate that :email is unique
  should validate that :password cannot be empty/falsy
  generates a confirmation token
  downcases email before validating

Finished in 0.20229 seconds (files took 2.1 seconds to load)
6 examples, 0 failures

Looks good; now, let’s proceed with the user presenter.

22.2. UserPresenter

Before we can create the users controller, we need a presenter to use all the awesome tools we implemented earlier.

Create a new file…

touch app/presenters/user_presenter.rb

and fill it with the following. We allow sorting, filtering and building with all the fields except confirmation_token and reset_password_token which cannot be used for sorting and filtering.

# app/presenters/user_presenter.rb
class UserPresenter < BasePresenter
  FIELDS = [:id, :email, :given_name, :family_name, :role, :last_logged_in_at,
           :confirmed_at, :confirmation_sent_at, :reset_password_sent_at,
           :created_at, :updated_at]

  sort_by    *FIELDS
  filter_by  *FIELDS
  build_with  *[FIELDS.push([:confirmation_token, :reset_password_token,
                             :confirmation_redirect_url,
                             :reset_password_redirect_url])].flatten
end

Time to create the controller.

22.3. UsersController

Create the controller file and its spec file with the command below.

touch app/controllers/users_controller.rb \
      spec/requests/users_spec.rb

Add the route to the config/routes.rb file.

# config/routes.rb
Rails.application.routes.draw do
  scope :api do
    resources :books, except: :put
    resources :authors, except: :put
    resources :publishers, except: :put
    resources :users, except: :put

    get '/search/:text', to: 'search#index'
  end

  root to: 'books#index'
end

Before we actually implement the controller, we need to add a mailer. When new users are created, we want an email to be sent to them so they can confirm their account.

Generate a new mailer.

rails g mailer UserMailer confirmation_email

Update the content of the app/mailers/application_mailer.rb file with the following. We just changed the “from” email.

# app/mailers/application_mailer.rb
class ApplicationMailer < ActionMailer::Base
  default from: 'admin@alexandria.com'
  layout 'mailer'
end

Next, we can add the code for the UserMailer class that will be responsible for sending the confirmation email. It is also going to update the confirmation_sent_at field of the user before sending.

# app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer

  def confirmation_email(user)
    @user = user
    @user.update_column(:confirmation_sent_at, Time.now)
    mail to: @user.email, subject: 'Confirm your Account!'
  end

end

Finally, here is the template we are going to use to send the email. There’s not much to see, except for the link pointing back to the Alexandria API that will confirm the account using the generated confirmation token.

<% # app/views/user_mailer/confirmation_email.text.erb %>
Hello,

Confirm your email by clicking on the link below:

CONFIRMATION LINK HERE

Welcome to Alexandria!

Update the tests for this email according to our changes.

# spec/mailers/user_mailer_spec.rb
require 'rails_helper'

RSpec.describe UserMailer, :type => :mailer do
  describe 'confirmation_email' do
    let(:user) { create(:user) }
    let(:mail) { UserMailer.confirmation_email(user) }

    it 'renders the headers' do
      expect(mail.subject).to eq('Confirm your Account!')
      expect(mail.to).to eq([user.email])
      expect(mail.from).to eq(['admin@alexandria.com'])
    end

    it 'renders the body' do
      expect(mail.body.encoded).to match('Hello')
    end
  end

end

You can delete the HTML template generated for the confirmation email; we won’t be using it.

rspec spec/mailers/user_mailer_spec.rb

Success (GREEN)

...

UserMailer
  confirmation_email
    renders the headers
    renders the body

Finished in 0.41258 seconds (files took 2.46 seconds to load)
2 examples, 0 failures

Let’s take a look at the users controller now. This time, I’m not going to show you how to write the tests. I believe you have the skills to write them yourself and you can reuse the code we wrote for the books controller. Feel free to check the code below before writing the tests.

# spec/requests/users_spec.rb
require 'rails_helper'

RSpec.describe 'Users', type: :request do

  before do
    allow_any_instance_of(UsersController).to(
      receive(:validate_auth_scheme).and_return(true))
    allow_any_instance_of(UsersController).to(
      receive(:authenticate_client).and_return(true))
  end

  let(:john) { create(:user) }
  let(:users) { [john] }

  describe 'GET /api/users' do
  end

  describe 'GET /api/users/:id' do
  end

  describe 'POST /api/users' do
  end

  describe 'PATCH /api/users/:id' do
  end

  describe 'DELETE /api/users/:id' do
  end
end

Most of the code is similar to the controllers we’ve built before except for the create action. There, we use…

UserMailer.send_confirmation(user).deliver_now

to send the confirmation email right away. In real-world applications, this kind of side action should be handled in a background process in order to return a response to the client as fast as possible.

To delay it, we could use deliver_later instead of deliver_now, but that would require setting up ActiveJob and something like Sidekiq which is outside of the scope of this book.

Another thing to note in this controller is in the user_params method:

def user_params
  params.require(:data).permit(:email, :password,
                               :given_name, :family_name,
                               :role, :confirmation_redirect_link)
end

We allow a parameter named confirmation_redirect_link to be sent and saved in the user record. But what is this field for? Well, it’s meant to contain a redirect URL sent by the client to which the server will redirect once the client has confirmed its account.

For example, let’s say a user signs up using our React front-end application. This user receives an email containing a confirmation link pointing to Alexandria. Once confirmed, we want to give the option to the client to redirect the user where it wants to. For the React front-end, that would be the login page available at http://alexandria/login (for example). For the iOS application, it could be an URI pointing that will trigger the application to open on the login page.

We will see in the next section what the server will do if the client doesn’t provide a redirect URL.

Here is the complete code of the users controller.

# app/controllers/users_controller.rb
class UsersController < ApplicationController

  def index
    users = orchestrate_query(User.all)
    render serialize(users)
  end

  def show
    render serialize(user)
  end

  def create
    if user.save
      UserMailer.confirmation_email(user).deliver_now
      render serialize(user).merge(status: :created, location: user)
    else
      unprocessable_entity!(user)
    end
  end

  def update
    if user.update(user_params)
      render serialize(user).merge(status: :ok)
    else
      unprocessable_entity!(user)
    end
  end

  def destroy
    user.destroy
    render status: :no_content
  end

  private

  def user
    @user ||= params[:id] ? User.find_by!(id: params[:id]) : User.new(user_params)
  end
  alias_method :resource, :user

  def user_params
    params.require(:data).permit(:email, :password,
                                 :given_name, :family_name,
                                 :role, :confirmation_redirect_url)
  end

end

If you’ve written some tests, it’s time to run them.

rspec spec/requests/users_spec.rb

Success (GREEN)

...

Finished in 0.94902 seconds (files took 2.27 seconds to load)
21 examples, 0 failures

22.4. Confirmation Controller

Users can now be created. We are not going to create a registrations controller to handle signups. Instead, we trust the client to implement that part and all we want is a call to POST /api/users with valid parameters.

But we need to ensure that users’ emails are correct before we enable their accounts. The only way to do this is by sending an email using the function we implemented.

Well, it’s time to create this user_confirmations controller!

touch app/controllers/user_confirmations_controller.rb \
      spec/requests/user_confirmations_spec.rb

Let’s not forget to add the route as well. We only need the show action for this controller. We rename the parameter in the URL from confirmation_token to id with param: :confirmation_token.

# config/routes.rb
Rails.application.routes.draw do
  scope :api do
    resources :books, except: :put
    resources :authors, except: :put
    resources :publishers, except: :put
    resources :users, except: :put

    resources :user_confirmations, only: :show, param: :confirmation_token

    get '/search/:text', to: 'search#index'
  end

  root to: 'books#index'
end

Let’s write a few simple tests for this new resource. Notice how we use the FactoryBot traits we created in the users factory earlier.

# spec/requests/user_confirmations_spec.rb
require 'rails_helper'

RSpec.describe 'UserConfirmations', type: :request do

  describe 'GET /api/user_confirmations/:confirmation_token' do

    context 'with existing token' do

      context 'with confirmation redirect url' do
        subject { get "/api/user_confirmations/#{john.confirmation_token}" }
        let(:john) { create(:user, :confirmation_redirect_url) }

        it 'redirects to http://google.com' do
          expect(subject).to redirect_to('http://google.com')
        end
      end

      context 'without confirmation redirect url' do
        let(:john) { create(:user, :confirmation_no_redirect_url) }
        before { get "/api/user_confirmations/#{john.confirmation_token}" }

        it 'returns HTTP status 200' do
          expect(response.status).to eq 200
        end

        it 'renders "Your are now confirmed!"' do
          expect(response.body).to eq 'You are now confirmed!'
        end
      end
    end

    context 'with nonexistent token' do
      before { get '/api/user_confirmations/fake' }

      it 'returns HTTP status 404' do
        expect(response.status).to eq 404
      end

      it 'renders "Token not found"' do
        expect(response.body).to eq 'Token not found'
      end
    end

  end
end

We also need to add a method, confirm, to the User model to update the fields that need to be updated once a user has been confirmed.

# app/models/user.rb
class User < ApplicationRecord
  # Hidden Code

  def confirm
    update_columns({
      confirmation_token: nil,
      confirmed_at: Time.now
    })
  end

  private

  def generate_confirmation_token # Hidden Code
  def downcase_email # Hidden Code
end

Let’s take a look at the controller. In the show action, we need to confirm the user and either redirect him or show him some text. Checking if a user with that confirmation token actually exists has been extracted into a before_action filter.

# app/controllers/user_confirmations_controller.rb
class UserConfirmationsController < ActionController::API
  before_action :confirmation_token_not_found

  def show
    user.confirm

    if user.confirmation_redirect_url
      redirect_to(user.confirmation_redirect_url)
    else
      render plain: 'You are now confirmed!'
    end
  end

  private

  def confirmation_token_not_found
    render(status: 404, plain: 'Token not found') unless user
  end

  def confirmation_token
    @confirmation_token ||= params[:confirmation_token]
  end

  def user
    @user ||= User.where(confirmation_token: confirmation_token).first
  end

end

Run the tests to ensure that we correctly implemented the confirmation process.

rspec spec/requests/user_confirmations_spec.rb

Success (GREEN)

Finished in 0.1999 seconds (files took 2.14 seconds to load)
5 examples, 0 failures

With a functional user confirmations controller, we can now update the confirmation email.

<% # app/views/user_mailer/confirmation_email.text.erb %>
Hello,

Confirm your email by clicking on the link below:

<%= link_to('Confirm Your Account',{
             controller: "user_confirmations",
             action: :show,
             confirmation_token: @user.confirmation_token
            }) %>

Welcome to Alexandria!
rspec

Success (GREEN)

...

Finished in 9.02 seconds (files took 2.25 seconds to load)
233 examples, 0 failures

If you want to try it manually, use the command below to send a request to create a new user. Replace the value of API_KEY with the key we generated in the previous chapter.

rails s
curl -X POST \
     -H "Authorization: Alexandria-Token api_key=1:my_api_key" \
     -H "Content-Type: application/json" \
     -d '{"data":{"email":"john@gmail.com",
          "password": "password",
          "confirmation_redirect_url":"http://google.com"}}' \
     http://localhost:3000/api/users

Output

{ "data":
  {
    "id":1,
    "email":"john@gmail.com",
    "given_name":null,
    "family_name":null,
    "role":"user",
    "last_logged_in_at":null,
    "confirmed_at":null,
    "confirmation_sent_at":"2016-06-08T09:38:53.708Z",
    "reset_password_sent_at":null,
    "created_at":"2016-06-08T09:38:53.688Z",
    "updated_at":"2016-06-08T09:38:53.688Z",
    "confirmation_token":"88047d1ee2fe8759b7107c58a4da10c2",
    "reset_password_token":null,
    "confirmation_redirect_url":"http://google.com",
    "reset_password_redirect_url":null
  }
}

You should see the email being sent in the logs:

UserMailer#confirmation_email: processed outbound mail in 44.6ms
Sent mail to john@gmail.com (5.7ms)
Date: Wed, 08 Jun 2016 15:54:44 +0700
From: admin@alexandria.com
To: john@gmail.com
Message-ID: <HIDDEN>
Subject: Confirm your Account!
Mime-Version: 1.0
Content-Type: text/plain;
 charset=UTF-8
Content-Transfer-Encoding: 7bit

Hello,

Confirm your email by clicking on the link below:

<a href="http://localhost:3000/api/user_confirmations/88047d1...">
  Confirm Your Account
</a>

Welcome to Alexandria!

Access the URL defined in the email and the account will get confirmed. You will also get redirected to Google. Great!

22.5. Password Reset

With the confirmation done, there is only one thing left in this chapter: providing users with a way to retrieve their lost passwords. The flow to reset a password will look like this:

  • Step 1: A user initiates the “reset password” process from a client. The client sends a request to the API with the user’s email and the redirect URL. The API also sends an email to the user containing a link to reset his password.
  • Step 2: The user clicks on the link in the email and is taken briefly to the Alexandria API before being redirected to the redirect URL given by the client. This redirect URL is mandatory and should take the user to a screen where he can change his password.
  • Step 3: The client sends a last request to the API with the new password.

To handle those tests, we will create a new controller and a “table-less” model.

Let’s get started.

22.5.1. Step 1 - Initiating The Password Reset Process

First, create the controller file.

touch app/controllers/password_resets_controller.rb \
      spec/requests/password_resets_spec.rb

Add the routes for this new controller in the config/routes.rb file. We only need the show and create actions.

# config/routes.rb
Rails.application.routes.draw do
  scope :api do
    resources :books, except: :put
    resources :authors, except: :put
    resources :publishers, except: :put
    resources :users, except: :put

    resources :user_confirmations, only: :show, param: :confirmation_token
    resources :password_resets, only: [:show, :create, :update],
                                param: :reset_token

    get '/search/:text', to: 'search#index'
  end

  root to: 'books#index'
end

Next, let’s write some tests for the endpoint that will initiate the reset password process. Available at /api/password_resets with the POST method, this resource will insert the information provided by the client in the user.

As the API creators, we know that it will only store them in the user record but from the outside, all the client cares about is that calling this URI will create a “password reset.”

At this point in the module, I’m not going to explain the tests anymore. The tests themselves should be enough to understand what’s going on since they document our intentions pretty well.

# spec/requests/password_resets_spec.rb
require 'rails_helper'

RSpec.describe 'PasswordResets', type: :request do

  let(:john) { create(:user) }

  describe 'POST /api/password_resets' do

    # This resource can only be accessed by an authenticated client.
    # For those steps, we will skip the authentication
    before do
      allow_any_instance_of(PasswordResetsController).to(
        receive(:validate_auth_scheme).and_return(true))
      allow_any_instance_of(PasswordResetsController).to(
        receive(:authenticate_client).and_return(true))
    end

    context 'with valid parameters' do
      let(:params) do
        {
          data: {
            email: john.email,
            reset_password_redirect_url: 'http://example.com'  
          }
        }
      end
      before { post '/api/password_resets', params: params }

      it 'returns 204' do
        expect(response.status).to eq 204
      end

      it 'sends the reset password email' do
        expect(ActionMailer::Base.deliveries.last.subject).to eq(
          'Reset your password'
        )
      end

      # Here we check that all the fields have properly been updated
      it 'adds the reset password attributes to "john"' do
        expect(john.reset_password_token).to be nil
        expect(john.reset_password_sent_at).to be nil
        updated = john.reload
        expect(updated.reset_password_token).to_not be nil
        expect(updated.reset_password_sent_at).to_not be nil
        expect(updated.reset_password_redirect_url).to eq 'http://example.com'
      end
    end

    context 'with invalid parameters' do
      let(:params) { { data: { email: john.email } } }
      before { post '/api/password_resets', params: params }

      it 'returns HTTP status 422' do
        expect(response.status).to eq 422
      end
    end

    context 'with nonexistent user' do
      let(:params) { { data: { email: 'fake@example.com' } } }
      before { post '/api/password_resets', params: params }

      it 'returns HTTP status 404' do
        expect(response.status).to eq 404
      end
    end
  end
end
rspec spec/requests/password_resets_spec.rb

Failure (RED)

Finished in 0.10167 seconds (files took 2.36 seconds to load)
5 examples, 5 failures

Let’s write enough code to make all those tests pass!

First, we need to add a new method in the UserMailer to send our new email. Write a test for it in the user_mailer_spec file.

# spec/mailers/user_mailer_spec.rb
require 'rails_helper'

RSpec.describe UserMailer, :type => :mailer do

  describe '#confirmation_email' # Hidden Code

  describe '#reset_password' do
    let(:user) { create(:user, :reset_password) }
    let(:mail) { UserMailer.reset_password(user) }

    it 'renders the headers' do
      expect(mail.subject).to eq('Reset your password')
      expect(mail.to).to eq([user.email])
      expect(mail.from).to eq(['admin@alexandria.com'])
    end

    it 'renders the body' do
      expect(mail.body.encoded).to match(
        'Use the link below to reset your password'
      )
    end
  end
end

Then add the reset_password method in the UserMailer.

# app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer

  def confirmation_email # Hidden Code

  def reset_password(user)
    @user = user
    @user.update_column(:confirmation_sent_at, Time.now)
    mail to: @user.email, subject: 'Reset your password'
  end

end

Create a new email template…

touch app/views/user_mailer/reset_password.text.erb

and fill it with a nice email to send users if they want to reset their passwords.

<% # app/views/user_mailer/reset_password.text.erb %>
Hello,

Use the link below to reset your password.

<%= link_to('Reset your password', {
            controller: "password_resets",
            action: :show,
            reset_token: @user.reset_password_token }) %>

If you didn't initiate this password reset, you can discard this email.
rspec spec/mailers/user_mailer_spec.rb

Success (GREEN)

UserMailer
  confirmation_email
    renders the headers
    renders the body
  #reset_password
    renders the headers
    renders the body

Finished in 0.44334 seconds (files took 2.36 seconds to load)
4 examples, 0 failures

We also want to add an easy-to-use method in the User model that will take care of updating all the fields related to the reset password process.

# app/models/user.rb
class User < ApplicationRecord
  # Hidden Code

  def confirm # Hidden Code

  def init_password_reset(redirect_url)
    assign_attributes({
      reset_password_token: SecureRandom.hex,
      reset_password_sent_at: Time.now,
      reset_password_redirect_url: redirect_url
    })
    save
  end

  private

  def generate_confirmation_token # Hidden Code
  def downcase_email # Hidden Code
end

We want to use the nice stuff that comes with ActiveRecord models like validations. This will allow us to write a cleaner controller that follows the logic of our previous ones.

To do this, we are going to create a “table-less” model named PasswordReset. Create a file for this special model.

touch app/models/password_reset.rb

And here is the first version of that “model”. Like a regular model, it will receive some parameters (email and reset_password_redirect_url) and respond to the valid? method if those parameters match our expectations.

The create method will be used to check that the reset request was correctly initialized by checking that a user is present, that the given parameters are valid and, finally, that the user was updated without any issue.

# app/models/password_reset.rb
class PasswordReset
  include ActiveModel::Model

  attr_accessor :email, :reset_password_redirect_url

  validates :email, presence: true
  validates :reset_password_redirect_url, presence: true

  def create
    user && valid? && user.init_password_reset(reset_password_redirect_url)
  end

  def user
    @user ||= retrieve_user
  end

  private

  def retrieve_user
    user = User.where(email: email).first
    raise ActiveRecord::RecordNotFound unless user
    user
  end

end

Here is the implementation for the controller and our first action, create. The create action will be used by clients to initiate a new password reset, while the show action will be accessed through the email sent to the user. Finally, the update action will be used to update the password at the end of the process.

Notice how we use the PasswordReset model to keep our controller DRY.

# app/controllers/password_resets_controller.rb
class PasswordResetsController < ApplicationController
  skip_before_action :validate_auth_scheme, only: :show
  skip_before_action :authenticate_client, only: :show

  def create
    if reset.create
      UserMailer.reset_password(reset.user).deliver_now
      render status: :no_content, location: reset.user
    else
      unprocessable_entity!(reset)
    end
  end

  private

  def reset
    @reset ||= reset = PasswordReset.new(reset_params)
  end

  def reset_params
    params.require(:data).permit(:email, :reset_password_redirect_url)
  end

end

Run the tests to see if everything is working as expected.

rspec spec/requests/password_resets_spec.rb

Success (GREEN)

...

PasswordResets
  POST /password_resets
    with valid parameters
      returns 204
      sends the reset password email
      adds the reset password attributes to "john"
    with invalid parameters
      returns HTTP status 422
    with nonexistent user
      returns HTTP status 404

Finished in 0.62079 seconds (files took 2.3 seconds to load)
5 examples, 0 failures

22.5.2. Step 2 - Redirecting The User

The second step is handling the link clicked in the reset email that was sent to the user.

Here are the tests for the /password_resets/:reset_token resource accessible with GET.

# spec/requests/password_resets_spec.rb
require 'rails_helper'

RSpec.describe 'PasswordResets', type: :request do

  let(:john) { create(:user) }

  describe 'POST /api/password_resets' # Hidden Code

  describe 'GET /api/password_resets/:reset_token' do

    context 'with existing user (valid token)' do
      subject { get "/api/password_resets/#{john.reset_password_token}" }

      context 'with redirect URL containing parameters' do
        let(:john) { create(:user, :reset_password) }

        it 'redirects to "http://example.com?some=params&reset_token=TOKEN"' do
          token = john.reset_password_token
          expect(subject).to redirect_to(
           "http://example.com?some=params&reset_token=#{token}"
          )
        end
      end

      context 'with redirect URL not containing any parameters' do
        let(:john) { create(:user, :reset_password_no_params) }

        it 'redirects to "http://example.com?reset_token=TOKEN"' do
          expect(subject).to redirect_to(
            "http://example.com?reset_token=#{john.reset_password_token}"
          )
        end
      end
    end

    context 'with nonexistent user' do
      before { get "/api/password_resets/123" }

      it 'returns HTTP status 404' do
        expect(response.status).to eq 404
      end
    end
  end # 'GET /password_resets/:reset_token'
end

Next, we need to update the PasswordReset model a bit. We want it to give us back the redirect URL containing the reset token. This token will be needed in the last step, so we need to give it to the client.

We also need to change the way we retrieve the user. For the create step, we are using the user’s email while in the update action, we use the reset_token sent by the client.

# app/models/password_reset.rb
class PasswordReset
  include ActiveModel::Model

  attr_accessor :email, :reset_password_redirect_url, :reset_token

  validates :email, presence: true
  validates :reset_password_redirect_url, presence: true

  def create
    user && valid? && user.init_password_reset(reset_password_redirect_url)
  end

  def redirect_url
    build_redirect_url
  end

  def user
    @user ||= retrieve_user
  end

  private

  def retrieve_user
    user = email ? user_with_email : user_with_token
    raise ActiveRecord::RecordNotFound unless user
    user
  end

  def user_with_email
    User.where(email: email).first
  end

  def user_with_token
    User.where(reset_password_token: reset_token).first
  end

  def build_redirect_url
    url = user.reset_password_redirect_url
    query_params = Rack::Utils.parse_query(URI(url).query)
    if query_params.any?
      "#{url}&reset_token=#{reset_token}"
    else
      "#{url}?reset_token=#{reset_token}"
    end
  end

end

Now, let’s add the show action to our controller. This action is super simple; all it does is instantiate the PasswordReset “model” and redirect to the URL given by the client earlier.

# app/controllers/password_resets_controller.rb
class PasswordResetsController < ApplicationController
  include ActiveModel::Model

  # Hidden Code

  def create # Hidden Code

  def show
    reset = PasswordReset.new({ reset_token: params[:reset_token] })
    redirect_to reset.redirect_url
  end

  private

  def reset # Hidden Code
  def reset_params # Hidden Code
end

All should be well when we run the tests.

rspec spec/requests/password_resets_spec.rb

Success (GREEN)

PasswordResets
  POST /password_resets
    with valid parameters
      returns 204
      sends the reset password email
      adds the reset password attributes to "john"
    with invalid parameters
      returns HTTP status 422
    with nonexistent user
      returns HTTP status 404
  GET /password_resets/:reset_token
    with existing user (valid token)
      with redirect URL containing parameters
        redirects to 'http://example.com?some=params&reset_token=TOKEN'
      with redirect URL not containing any parameters
        redirects to 'http://example.com?reset_token=TOKEN'
    with nonexistent user
      returns HTTP status 404

Finished in 0.63131 seconds (files took 2.35 seconds to load)
8 examples, 0 failures

22.5.3. Step 3 - Updating The Password

Finally, the last step! As usual, let’s add some tests that will test the password update.

# spec/requests/password_resets_spec.rb
require 'rails_helper'

RSpec.describe 'PasswordResets', type: :request do

  let(:john) { create(:user) }

  describe 'POST /api/password_resets' # Hidden Code
  describe 'GET /api/password_resets/:reset_token' # Hidden Code

  describe 'PATCH /api/password_resets/:reset_token' do

    before do
      allow_any_instance_of(PasswordResetsController).to(
        receive(:validate_auth_scheme).and_return(true))
      allow_any_instance_of(PasswordResetsController).to(
        receive(:authenticate_client).and_return(true))
    end

    context 'with existing user (valid token)' do
      let(:john) { create(:user, :reset_password) }
      before do
        patch "/api/password_resets/#{john.reset_password_token}", params: params
      end

      context 'with valid parameters' do
        let(:params) { { data: { password: 'new_password' } } }

        it 'returns HTTP status 204' do
          expect(response.status).to eq 204
        end

        it 'updates the password' do
          expect(john.reload.authenticate('new_password')).to_not be false
        end
      end

      context 'with invalid parameters' do
        let(:params) { { data: { password: '' } } }

        it 'returns HTTP status 422' do
          expect(response.status).to eq 422
        end
      end
    end

    context 'with nonexistent user' do
      before do
        patch '/api/password_resets/123', params: {
          data: { password: 'password' }
        }
      end

      it 'returns HTTP status 404' do
        expect(response.status).to eq 404
      end
    end
  end # 'PATCH /password_resets/:reset_token' end
end

Let’s add another little method, complete_password_reset, in the User model to update all the required fields.

# app/models/user.rb
class User < ApplicationRecord
  # Hidden Code

  def confirm # Hidden Code
  def init_password_reset # Hidden Code

  def complete_password_reset(password)
    assign_attributes({
      password: password,
      reset_password_token: nil,
      reset_password_sent_at: nil,
      reset_password_redirect_url:  nil
    })
    save
  end

  private

  def generate_confirmation_token # Hidden Code
  def downcase_email # Hidden Code
end

Here is the last iteration of the PasswordReset “model.” Now that we need to handle updating the password, we have to define two sets of validations. The email and redirect_url are required only when initiating a password reset. For the completion, only the password is needed.

To avoid creating another table-less model, we can just use conditional validations using an attribute named updating that will be set to true only in the update method.

# app/models/password_reset.rb
class PasswordReset
  include ActiveModel::Model

  attr_accessor :email, :reset_password_redirect_url, :password,
                :reset_token, :updating

  validates :email, presence: true, unless: :updating
  validates :reset_password_redirect_url, presence: true, unless: :updating
  validates :password, presence: true, if: :updating

  def create
    user && valid? && user.init_password_reset(reset_password_redirect_url)
  end

  def redirect_url
    build_redirect_url
  end

  def update
    self.updating = true
    user && valid? && user.complete_password_reset(password)
  end

  def user
    @user ||= retrieve_user
  end

  private

  def retrieve_user
    user = email ? user_with_email : user_with_token
    raise ActiveRecord::RecordNotFound unless user
    user
  end

  def user_with_email
    User.where(email: email).first
  end

  def user_with_token
    User.where(reset_password_token: reset_token).first
  end

  def build_redirect_url
    url = user.reset_password_redirect_url
    query_params = Rack::Utils.parse_query(URI(url).query)
    if query_params.any?
      "#{url}&reset_token=#{reset_token}"
    else
      "#{url}?reset_token=#{reset_token}"
    end
  end

end

The update action in the password_resets controller looks just like the create one. The only differences are that we call a different method on the reset instance and we don’t send an email in the update action.

# app/controllers/password_resets_controller.rb
class PasswordResetsController < ApplicationController
  skip_before_action :validate_auth_scheme, only: :show
  skip_before_action :authenticate_client, only: :show

  def show
    reset = PasswordReset.new({ reset_token: params[:reset_token] })
    redirect_to reset.redirect_url
  end

  def create
    if reset.create
      UserMailer.reset_password(reset.user).deliver_now
      render status: :no_content, location: reset.user
    else
      unprocessable_entity!(reset)
    end
  end

  def update
    reset.reset_token = params[:reset_token]
    if reset.update
      render status: :no_content
    else
      unprocessable_entity!(reset)
    end
  end

  private

  def reset
    @reset ||= reset = PasswordReset.new(reset_params)
  end

  def reset_params
    params.require(:data).permit(:email, :reset_password_redirect_url, :password)
  end
end

Are we done yet? Let’s run the tests to answer that question.

rspec spec/requests/password_resets_spec.rb

Success (GREEN)

...

Finished in 0.71258 seconds (files took 2.28 seconds to load)
12 examples, 0 failures
Box 22.1. DRY up

We could reduce the code duplication even more by creating a shared method that receives a block, but that would probably make the code confusing. Here’s an example anyway of what we could do.

def create
 handle_password_reset(:create) do
   UserMailer.reset_password(reset.user).deliver_now
 end
end

def update
 reset.reset_token = params[:reset_token]
 handle_password_reset(:update)
end

def handle_password_reset(method)
 if reset.send(method)
   yield if block_given?
   render status: :no_content
 else
   unprocessable_entity!(reset)
 end
end

22.6. Integration Tests

Integration tests can be used to test entire feature; for example, the entire reset password flow.

mkdir spec/features && \
  touch spec/features/password_reset_flow_spec.rb
# spec/features/password_reset_flow_spec.rb
require 'rails_helper'

RSpec.describe 'Password Reset Flow', type: :request do

  let(:john) { create(:user) }
  let(:api_key) { ApiKey.create }
  let(:headers) do
    { 'HTTP_AUTHORIZATION' =>
        "Alexandria-Token api_key=#{api_key.id}:#{api_key.key}" }
  end
  let(:create_params) do
    { email: john.email, reset_password_redirect_url: 'http://example.com'  }
  end
  let(:update_params) { { password: 'new_password' } }

  it 'resets the password' do
    expect(john.authenticate('password')).to_not be false
    expect(john.reset_password_token).to be nil

    # Step 1
    post '/api/password_resets', params: { data: create_params }, headers: headers
    expect(response.status).to eq 204
    reset_token = john.reload.reset_password_token
    expect(ActionMailer::Base.deliveries.last.body).to match reset_token

    # Step 2
    sbj = get "/api/password_resets/#{reset_token}"
    expect(sbj).to redirect_to("http://example.com?reset_token=#{reset_token}")

    # Step 3
    patch "/api/password_resets/#{reset_token}",
          params: { data: update_params }, headers: headers
    expect(response.status).to eq 204
    expect(john.reload.authenticate('new_password')).to_not be false
  end

end

Run this test.

rspec spec/features/password_reset_flow_spec.rb

Success (GREEN)

...

Password Reset Flow
  resets the password

Finished in 0.46872 seconds (files took 1.96 seconds to load)
1 example, 0 failures

22.7. Pushing Our Changes

Run all the tests to ensure that we didn’t break anything.

rspec

Success (GREEN)

Finished in 15.72 seconds (files took 4 seconds to load)
248 examples, 0 failures

Let’s push our changes.

git status

Output

On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	modified:   Gemfile
	modified:   Gemfile.lock
	modified:   app/mailers/application_mailer.rb
	modified:   config/routes.rb
	modified:   db/schema.rb

Untracked files:
  (use "git add <file>..." to include in what will be committed)

	app/controllers/password_resets_controller.rb
	app/controllers/user_confirmations_controller.rb
	app/controllers/users_controller.rb
	app/mailers/user_mailer.rb
	app/models/password_reset.rb
	app/models/user.rb
	app/presenters/user_presenter.rb
	app/views/user_mailer/
	db/migrate/20160609070848_create_users.rb
	spec/factories/users.rb
	spec/fixtures/
	spec/mailers/
	spec/models/user_spec.rb
	spec/requests/password_resets_spec.rb
	spec/requests/user_confirmations_spec.rb
	spec/requests/users_spec.rb

no changes added to commit (use "git add" and/or "git commit -a")

Stage them.

git add .

Commit the changes.

git commit -m "Implement Users"

Output

[master 962cce6] Implement Users
 22 files changed, 845 insertions(+), 6 deletions(-)
 create mode 100644 app/controllers/password_resets_controller.rb
 create mode 100644 app/controllers/user_confirmations_controller.rb
 create mode 100644 app/controllers/users_controller.rb
 create mode 100644 app/mailers/user_mailer.rb
 create mode 100644 app/models/password_reset.rb
 create mode 100644 app/models/user.rb
 create mode 100644 app/presenters/user_presenter.rb
 create mode 100644 app/views/user_mailer/confirmation_email.text.erb
 create mode 100644 app/views/user_mailer/reset_password.text.erb
 create mode 100644 db/migrate/20160609070848_create_users.rb
 create mode 100644 spec/factories/users.rb
 create mode 100644 spec/fixtures/user_mailer/confirmation_email
 create mode 100644 spec/mailers/user_mailer_spec.rb
 create mode 100644 spec/models/user_spec.rb
 create mode 100644 spec/requests/password_resets_spec.rb
 create mode 100644 spec/requests/user_confirmations_spec.rb
 create mode 100644 spec/requests/users_spec.rb

Push to GitHub.

git push origin master

22.8. Wrap Up

This chapter was a big one! In it, we implemented all the logic needed for users to create accounts, confirm them and reset their passwords if needed. In the next chapter, we are going to add the user authentication part.