Learn Ruby on Rails Book

Adding logout feature

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]
3
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
14end
15
16  def destroy
17    @current_user = nil
18  end
19
20  private
21
22    def login_params
23      params.require(:login).permit(:email, :password)
24    end
25end

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]
3  resources :users, only: :create
4  resource :sessions, only: [:create, :destroy]
5
6  root "home#index"
7  get '*path', to: 'home#index', via: :all
8end
9

If you are wondering what the %i[] notation is, then it's nothing but a non-interpolated array of symbols, separated by white-space. If we want to group n-number of symbols in an array format, then use %i[symbol1 symbol2] notation. It saves you from adding a : symbol for each of the symbols like we have written the sessions resource above in routes.rb. Also it's much more neat that way and helps with readability. Also in the routes, if you look closely, you will notice that the tasks resources is written with an except rather than using only. As a general rule of thumb, always prefer using except if the number of items in only is greater than 3. This is also to make the code cleaner.

Now we will be making a DELETE request to the route /sessions and passing authentication_token and email in the request header, to you know, verify that's it's actually us who is making the request.

Open apis/auth.js and add following lines.

1import axios from "axios";
2
3const login = payload => axios.post("/sessions", payload);
4
5const logout = () => axios.delete(`/sessions`);
6
7const signup = payload => axios.post("/users", payload);
8
9const authApi = {
10  login,
11  logout,
12  signup,
13};
14
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"];
4};

Before we add code to the application view file, let's create navigation components.

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:

1  //------previous code -------
2
3import { getFromLocalStorage, setToLocalStorage } from "helpers/storage";
4
5const NavBar = () => {
6
7  //------previous code -------
8
9  const handleLogout = async () => {
10    try {
11      await authApi.logout();
12      setToLocalStorage({ authToken: null,
13                          email: null,
14                          userId: null,
15                          userName: null });
16      resetAuthTokens();
17      window.location.href = "/";
18    } catch (error) {
19      logger.error(error);
20    }
21  };
22
23  return (
24
25    //------previous code -------
26
27            <a
28              onClick={handleLogout}
29              className="inline-flex items-center px-1 pt-1 text-sm
30             font-semibold leading-5 text-bb-gray-600 text-opacity-50
31             transition duration-150 ease-in-out border-b-2
32             border-transparent hover:text-bb-gray-600 focus:outline-none
33              focus:text-bb-gray-700 cursor-pointer"
34            >
35              LogOut
36            </a>
37          </div>
38        </div>
39      </div>
40    </nav>
41
42  );
43};
44
45export default NavBar;

Once the logout button(or the simple anchor tag) is clicked, the logout API is called and all the data stored in local storage is cleared. We are making use of the setToLocalStorage helper method created in the previous section. The user will be then redirected to the login page if the logout was successfully done in server too, where we just destroy the @current_user instance variable.

Storing information about who created the task

Let's add a new column creator_id to tasks table. To add a new column to our existing table, we'll generate 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
5end

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]
4
5  # ----- previous code ------
6
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', 'Task') }
11    else
12      errors = @task.errors.full_messages.to_sentence
13      render status: :unprocessable_entity, json: { errors: errors }
14    end
15  end
16
17  # ----- previous code ------
18
19  private
20
21  def task_params
22    params.require(:task).permit(:title)
23  end
24
25  # ----- previous code ------
26end
27

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 ability to logout"