Learn Ruby on Rails Book

Adding logout feature


Until now, users once logged in, had no way of logging out of our application. Let us implement the logout feature:

  • We can add a logout button in our nav bar.
  • Clicking the logout button should terminate the session and redirect the user to the login page.
  • From now onwards, we should keep track of who created which task. Later, we can use this to limit access to a task to its creator and its assignee only.

Technical design

We will do the following to implement the given requirements:

  • We will create a destroy action in SessionsController. We use this action to clean up the session on the backend when the user logs out. We can map this action to the DELETE request to the /sessions URL.
  • After calling the API, we will clear our browser's local storage and reset the headers from axios configuration.
  • Now, to add information about the task creator, we will need a new column creator_id in tasks table.
  • We will get the currently logged-in user details via the @current_user instance variable. We can pass the @current_user.id value as creator_id while creating tasks.

Sessions controller

Open app/controllers/sessions_controller.rb and add the following lines.

1class SessionsController < ApplicationController
2  before_action :authenticate_user_using_x_auth_token, only: [:destroy]
4  def create
5    user = User.find_by(email: login_params[:email].downcase)
6    if user.present? && user.authenticate(login_params[:password])
7      render status: :ok, json: { auth_token: user.authentication_token,
8                                  userId: user.id,
9                                  user_name: user.name }
10    else
11      render status: :unauthorized, json: { notice: t('session.incorrect_credentials') }
12    end
13  end
15  def destroy
16    @current_user = nil
17    # any other session cleanup tasks can be done here...
18  end
20  private
22    def login_params
23      params.require(:login).permit(:email, :password)
24    end

Session routes

Let's modify routes.rb and add a destroy action for session:

1Rails.application.routes.draw do
2  resources :tasks, except: %i[new edit], param: :slug
3  resources :users, only: :create
4  resource :sessions, only: %i[create destroy]
6  root "home#index"
7  get '*path', to: 'home#index', via: :all

If you are wondering what the %i[] notation is, then it's a way of creating an array where elements are separated by space. ["Ruby", "Python", "PHP"] is same as %i[Ruby Python PHP]. As we can see the latter version is much cleaner to look at.

In the routes, if you look closely, you will notice that the tasks resources is written with an except rather than using only.

Frontend changes

Open apis/auth.js and add following lines.

1import axios from "axios";
3const login = (payload) => axios.post("/sessions", payload);
5const logout = () => axios.delete(`/sessions`);
7const signup = (payload) => axios.post("/users", payload);
9const authApi = {
10  login,
11  logout,
12  signup,
15export default authApi;

Let's open apis/axios.js and create a function to clear the default Axios headers when the user is logged out. Add the following code at the end of the file:

1export const resetAuthTokens = () => {
2  delete axios.defaults.headers["X-Auth-Email"];
3  delete axios.defaults.headers["X-Auth-Token"];

Now, let's open the NavBar/index.jsx component and make use of the logout API that we added before to make the logout logic complete. Replace the whole content of NavBar/index.jsx with this code:

1import React from "react";
2import NavItem from "./NavItem";
3import authApi from "apis/auth";
4import { resetAuthTokens } from "src/apis/axios";
5import { getFromLocalStorage, setToLocalStorage } from "helpers/storage";
7const NavBar = () => {
8  const userName = getFromLocalStorage("authUserName");
9  const handleLogout = async () => {
10    try {
11      await authApi.logout();
12      setToLocalStorage({
13        authToken: null,
14        email: null,
15        userId: null,
16        userName: null,
17      });
18      resetAuthTokens();
19      window.location.href = "/";
20    } catch (error) {
21      logger.error(error);
22    }
23  };
25  return (
26    <nav className="bg-white shadow">
27      <div className="px-2 mx-auto max-w-7xl sm:px-4 lg:px-8">
28        <div className="flex justify-between h-16">
29          <div className="flex px-2 lg:px-0">
30            <div className="hidden lg:flex">
31              <NavItem name="Todos" path="/" />
32              <NavItem
33                name="Create"
34                iconClass="ri-add-fill"
35                path="/tasks/create"
36              />
37              <NavItem name="Preferences" path="/my/preferences" />
38            </div>
39          </div>
40          <div className="flex items-center justify-end gap-x-4">
41            <span
42              className="inline-flex items-center px-2 pt-1 text-sm
43              font-regular leading-5 text-bb-gray-600 text-opacity-50
44              transition duration-150 ease-in-out border-b-2
45              border-transparent focus:outline-none
46              focus:text-bb-gray-700"
47            >
48              {userName}
49            </span>
51            <a
52              onClick={handleLogout}
53              className="inline-flex items-center px-1 pt-1 text-sm
54              font-semibold leading-5 text-bb-gray-600 text-opacity-50
55              transition duration-150 ease-in-out border-b-2
56              border-transparent hover:text-bb-gray-600 focus:outline-none
57              focus:text-bb-gray-700 cursor-pointer"
58            >
59              LogOut
60            </a>
61          </div>
62        </div>
63      </div>
64    </nav>
65  );
68export default NavBar;

We are making use of the setToLocalStorage helper method created in the previous section to clear the local storage. The user will be then redirected to the login page if the logout was successfully done in the server too, where we destroy the @current_user instance variable.

Now let's commit these changes:

1git add -A
2git commit -m "Added ability to logout"

Storing information about who created the task

As discussed earlier, let's add a new column creator_id to tasks table using a a new migration.

1rails g migration AddCreatorIdToTask creator_id:integer
1class AddCreatorIdToTask < ActiveRecord::Migration[6.0]
2  def change
3    add_column :tasks, :creator_id, :integer
4  end

The code for the change method will be auto-populated when we run the above command. The reason is that, our migration command is of form add_column_name_to_table_name column_name:type which Rails will infer from and hence generates the code for that, even respecting that types passed in.

Cool right? That's one among the many Rails magic that you will be seeing throughout this book!

Again, once we create a new migration, let's persist that into our database:

1bundle exec rails db:migrate

Setting the creator id of a task

We always want the creator_id of a task to be set as the id of the current_user, because the current user is the one who creates the task.

To implement that, we need to change the create action of Tasks controller, as follows:

1class TasksController < ApplicationController
2  before_action :authenticate_user_using_x_auth_token
3  before_action :load_task, only: %i[show update destroy]
5  # ----- previous code ------
7  def create
8    @task = Task.new(task_params.merge(creator_id: @current_user.id))
9    if @task.save
10      render status: :ok, json: { notice: t('successfully_created', entity: 'Task') }
11    else
12      errors = @task.errors.full_messages.to_sentence
13      render status: :unprocessable_entity, json: { errors: errors }
14    end
15  end
17  # ----- previous code ------
19  private
21  def task_params
22    params.require(:task).permit(:title)
23  end
25  # ----- previous code ------

With this change, the created task always has its creator_id set to that of the current_user.

Now let's commit these changes:

1git add -A
2git commit -m "Added creator id to tasks"
    to navigateEnterto select Escto close