123456789_123456789_123456789_123456789_123456789_

DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.

Sign Up and Settings

This guide covers adding Sign Up and Settings to the store e-commerce application in the Getting Started Guide. We will use the final code from that guide as a starting place.

After reading this guide, you will know how to:


Introduction

One of the most common features to add to any application is a sign up process for registering new users. The e-commerce application we've built so far only has authentication and users must be created in the Rails console or a script.

This feature is required before we can add other features. For example, to let users create wishlists, they will need to be able to sign up first before they can create a wishlist associated with their account.

Let's get started!

Adding Sign Up

We've already used the Rails authentication generator in the Getting Started guide to allow users to login to their accounts. The generator created a User model with email_address:string and password_digest:string columns in the database. It also added has_secure_password to the User model which handles passwords and confirmations. This takes care of most of what we need to add sign up to our application.

Adding Names To Users

It's also a good idea to collect the user's name at sign up. This allows us to personalize their experience and address them directly in the application. Let's start by adding first_name and last_name columns to the database.

In the terminal, create a migration with these columns:

$ bin/rails g migration AddNamesToUsers first_name:string last_name:string

Then migrate the database:

$ bin/rails db:migrate

Let's also add a method to combine first_name and last_name, so that we can display the user's full name.

Open app/models/user.rb and add the following:

class User < ApplicationRecord
  has_secure_password
  has_many :sessions, dependent: :destroy

  normalizes :email_address, with: ->(e) { e.strip.downcase }

  validates :first_name, :last_name, presence: true

  def full_name
    "#{first_name} #{last_name}"
  end
end

TIP: has_secure_password only validates the presence of the password. Consider adding more validations for password minimum length or complexity to improve security.

Next, let's add sign up so we can register new users.

Sign Up Routes & Controller

Now that our database has all the necessary columns to register new users, the next step is to create a route for sign up and its matching controller.

In config/routes.rb, let's add a resource for sign up:

resource :session
resources :passwords, param: :token
resource :sign_up

We're using a singular resource here because we want a singular route for /sign_up.

This route directs requests to app/controllers/sign_ups_controller.rb so let's create that controller file now.

class SignUpsController < ApplicationController
  def show
    @user = User.new
  end
end

We're using the show action to create a new User instance, which will be used to display the sign up form.

Let's create the form next. Create app/views/sign_ups/show.html.erb with the following code:

<h1>Sign Up</h1>

<%= form_with model: @user, url: sign_up_path do |form| %>
  <% if form.object.errors.any? %>
    <div>Error: <%= form.object.errors.full_messages.first %></div>
  <% end %>

  <div>
    <%= form.label :first_name %>
    <%= form.text_field :first_name, required: true, autofocus: true, autocomplete: "given-name" %>
  </div>

  <div>
    <%= form.label :last_name %>
    <%= form.text_field :last_name, required: true, autocomplete: "family-name" %>
  </div>

  <div>
    <%= form.label :email_address %>
    <%= form.email_field :email_address, required: true, autocomplete: "email" %>
  </div>

  <div>
    <%= form.label :password %>
    <%= form.password_field :password, required: true, autocomplete: "new-password" %>
  </div>

  <div>
    <%= form.label :password_confirmation %>
    <%= form.password_field :password_confirmation, required: true, autocomplete: "new-password" %>
  </div>

  <div>
    <%= form.submit "Sign up" %>
  </div>
<% end %>

This form collects the user's name, email, and password. We're using the autocomplete attribute to help the browser suggest the values for these fields based on the user's saved information.

You'll also notice we set url: sign_up_path in the form alongside model: @user. Without this url: argument, form_with would see we have a User and send the form to /users by default. Since we want the form to submit to /sign_up, we set the url: to override the default route.

Back in app/controllers/sign_ups_controller.rb we can handle the form submission by adding the create action.

class SignUpsController < ApplicationController
  def show
    @user = User.new
  end

  def create
    @user = User.new(sign_up_params)
    if @user.save
      start_new_session_for(@user)
      redirect_to root_path
    else
      render :show, status: :unprocessable_entity
    end
  end

  private
    def sign_up_params
      params.expect(user: [ :first_name, :last_name, :email_address, :password, :password_confirmation ])
    end
end

The create action assigns parameters and attempts to save the user to the database. If successful, it logs the user in and redirects to root_path, otherwise it re-renders the form with errors.

Visit http://localhost:3000/sign_up to try it out.

Requiring Unauthenticated Access

Authenticated users can still access SignUpsController and create another account while they're logged in which is confusing.

Let's fix this by adding a helper to the Authentication module in app/controllers/concerns/authentication.rb.

module Authentication
  extend ActiveSupport::Concern

  included do
    before_action :require_authentication
    helper_method :authenticated?
  end

  class_methods do
    def allow_unauthenticated_access(**options)
      skip_before_action :require_authentication, **options
    end

    def unauthenticated_access_only(**options)
      allow_unauthenticated_access **options
      before_action -> { redirect_to root_path if authenticated? }, **options
    end

    # ...

The unauthenticated_access_only class method can be used in any controller where we want to restrict actions to unauthenticated users only.

We can then use this method at the top of SignUpsController.

class SignUpsController < ApplicationController
  unauthenticated_access_only

  # ...
end

Rate Limiting Sign Up

Our application will be accessible on the internet so we're bound to have malicious bots and users trying to spam our application. We can add rate limiting to sign up to slow down anyone submitting too many requests.

Rails makes this easy with the rate_limit</a> method in controllers.

class SignUpsController < ApplicationController
  unauthenticated_access_only
  rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to sign_up_path, alert: "Try again later." }

  # ...
end

This will block any form submissions that happen more than 10 times within 3 minutes.

Editing Passwords

Now that users can login, let's create all the usual places that users would expect to update their profile, password, email address, and other settings.

Using Namespaces

The Rails authentication generator already created a controller at app/controllers/passwords_controller.rb for password resets. This means we need to use a different controller for editing passwords of authenticated users.

To prevent conflicts, we can use a feature called namespaces. A namespace organizes routes, controllers, and views into folders and helps prevent conflicts like our two passwords controllers.

We'll create a namespace called "Settings" to separate out the user and store settings from the rest of our application.

In config/routes.rb we can add the Settings namespace along with a resource for editing passwords:

namespace :settings do
  resource :password, only: [ :show, :update ]
end

This will generate a route for /settings/password for editing the current user's password which is separate from the password resets routes at /password.

Adding the Namespaced Passwords Controller & View

Namespaces also move controllers into a matching module in Ruby. This controller will be in a settings folder to match the namespace.

Let's create the folder and controller at app/controllers/settings/passwords_controller.rb and start with the show action.

class Settings::PasswordsController < ApplicationController
  def show
  end
end

Views also move to a settings folder so let's create the folder and view at app/views/settings/passwords/show.html.erb for this action.

<h1>Password</h1>

<%= form_with model: {Current.user}, url: settings_password_path do |form| %>
  <% if form.object.errors.any? %>
    <div><%= form.object.errors.full_messages.first %></div>
  <% end %>

  <div>
    <%= form.label :password_challenge %>
    <%= form.password_field :password_challenge, required: true, autocomplete: "current-password" %>
  </div>

  <div>
    <%= form.label :password %>
    <%= form.password_field :password, required: true, autocomplete: "new-password" %>
  </div>

  <div>
    <%= form.label :password_confirmation %>
    <%= form.password_field :password_confirmation, required: true, autocomplete: "new-password" %>
  </div>

  <div>
    <%= form.submit "Update password" %>
  </div>
<% end %>

We've set the url: argument to ensure the form submits to our namespaced route and is processed by the Settings::PasswordsController.

Passing model: {Current.user} also tells form_with to submit a PATCH request to process the form with the update action.

TIP: Current.user comes from CurrentAttributes which is a per-request attribute which resets automatically before and after each request. The Rails authentication generator uses this to keep track of the logged in User.

Safely Updating Passwords

Let's add that update action to the controller now.

class Settings::PasswordsController < ApplicationController
  def show
  end

  def update
    if Current.user.update(password_params)
      redirect_to settings_profile_path, status: :see_other, notice: "Your password has been updated."
    else
      render :show, status: :unprocessable_entity
    end
  end

  private
    def password_params
      params.expect(user: [ :password, :password_confirmation, :password_challenge ]).with_defaults(password_challenge: "")
    end
end

For security, we need to ensure that the user is the only one who can update their password. The has_secure_password method in our User model provides this attribute. If password_challenge is present, it will validate the password challenge against the user's current password in the database to confirm it matches.

A malicious user could try deleting the password_challenge field in the browser to bypass this validation. To prevent this and ensure the validation always runs, we use .with_defaults(password_challenge: "") to set a default value even if the password_challenge parameter is missing.

Now let's add the route to redirect the user once the password is updated.

namespace :settings do
  resource :password, only: [ :show, :update ]
  resource :profile, only: [ :show ]
end

You can now visit http://localhost:3000/settings/password to update your password.

Renaming The Password Challenge Attribute

While password_challenge is a good name for our code, users are used to seeing "Current password" for this form field. We can rename this with locales in Rails to change how this attribute is displayed on the frontend.

Add the following to config/locales/en.yml:

en:
  hello: "Hello world"
  products:
    index:
      title: "Products"

  activerecord:
    attributes:
      user:
        password_challenge: "Current password"

To learn more, check out the I18n Guide

Editing User Profiles

Next, let's add a page so users can edit their profile, like updating their first and last name.

Profile Routes & Controller

In config/routes.rb, add an update action on the profile resource under the settings namespace. We can also add a root to the namespace to handle any visits to /settings and redirect them to profile settings.

namespace :settings do
  resource :password, only: [ :show, :update ]
  resource :profile, only: [ :show, :update ]

  root to: redirect("/settings/profile")
end

Let's create our controller for editing profiles at app/controllers/settings/profiles_controller.rb.

class Settings::ProfilesController < ApplicationController
  def show
  end

  def update
    if Current.user.update(profile_params)
      redirect_to settings_profile_path, status: :see_other, notice: "Your profile was updated successfully."
    else
      render :show, status: :unprocessable_entity
    end
  end

  private
    def profile_params
      params.expect(user: [ :first_name, :last_name ])
    end
end

This is very similar to the passwords controller but only allows updating the user's profile details like first and last name.

Then create app/views/settings/profiles/show.html.erb to show the edit profile form.

<h1>Profile</h1>

<%= form_with model: {Current.user}, url: settings_profile_path do |form| %>
  <% if form.object.errors.any? %>
    <div>Error: <%= form.object.errors.full_messages.first %></div>
  <% end %>

  <div>
    <%= form.label :first_name %>
    <%= form.text_field :first_name, required: true, autocomplete: "given-name" %>
  </div>

  <div>
    <%= form.label :last_name %>
    <%= form.text_field :last_name, required: true, autocomplete: "family-name" %>
  </div>

  <div>
    <%= form.submit "Update profile" %>
  </div>
<% end %>

You can now visit http://localhost:3000/settings/profile to update your name.

Updating Navigation

Let's update the navigation to include a link to Settings next to the Log out button.

Open app/views/layouts/application.html.erb and update the navbar.

<!DOCTYPE html>
<html>
  <head>
    <%# ... %>
  </head>

  <body>
    <div class="notice"><%= flash[:notice] %></div>
    <div class="alert"><%= flash[:alert] %></div>

    <nav class="navbar">
      <%= link_to "Home", root_path %>
      <% if authenticated? %>
        <%= link_to "Settings", settings_root_path %>
        <%= button_to "Log out", session_path, method: :delete %>
      <% else %>
        <%= link_to "Sign Up", sign_up_path %>
        <%= link_to "Login", new_session_path %>
      <% end %>
    </nav>

You'll now see a Settings link in the navbar when authenticated.

Settings Layout

While we're here, let's add a new layout for Settings so we can organize them in a sidebar. To do this, we're going to use a Nested Layout.

A nested layout allows you add HTML (like a sidebar) while still rendering the application layout. This means we don't have to duplicate our head tags or navigation in our Settings layout.

Let's create app/views/layouts/settings.html.erb and add the following:

<%= content_for :content do %>
  <section class="settings">
    <nav>
      <h4>Account Settings</h4>
      <%= link_to "Profile", settings_profile_path %>
      <%= link_to "Password", settings_password_path %>
    </nav>

    <div>
      <%= yield %>
    </div>
  </section>
<% end %>

<%= render template: "layouts/application" %>

In the settings layout, we're providing HTML for the sidebar and telling Rails to render the application layout as the parent.

We need to modify the application layout to render the content from the nested layout using yield(:content).

<!DOCTYPE html>
<html>
  <head>
    <%# ... %>
  </head>

  <body>
    <div class="notice"><%= flash[:notice] %></div>
    <div class="alert"><%= flash[:alert] %></div>

    <nav class="navbar">
      <%= link_to "Home", root_path %>
      <% if authenticated? %>
        <%= link_to "Settings", settings_root_path %>
        <%= button_to "Log out", session_path, method: :delete %>
      <% else %>
        <%= link_to "Sign Up", sign_up_path %>
        <%= link_to "Login", new_session_path %>
      <% end %>
    </nav>

    <main>
      <%= content_for?(:content) ? yield(:content) : yield %>
    </main>
  </body>
</html

This allows the application controller to be used normally with yield or it can be a parent layout if content_for(:content) is used in a nested layout.

We now have two separate