Skip to main content Link Search Menu Expand Document (external link)

User authentication

Most of the applications require users to verify themselves. Usually, we use a unique user id like a username or email and password when signing up. We use this to identify the users of our applications. In this lesson, we will learn how to work with authenticated users.

Table of contents

  1. Devise
  2. Reference users to posts.
  3. Reference users to comments

Devise

This is one of the very popular gems that we use in Ruby on Rails. It handles the authentication for us with minimal work.

Our main goal in this lesson is to add an authentication feature in our application and we’ll be using the devise gem for that.

You need to install devise to your application before proceeding to the next step. Please read our Devise documentation and learn how to add it in your application.

Reference users to posts.

In our current project, anyone can create a post, edit, and delete any post. Now that we have devise installed in our application.

Our tasks are:

  • Preventing those unauthenticated users from creating, editing, and deleting posts.
  • Allow authenticated users to create new posts.
  • Only the author of the post can edit or delete their posts.

In the terminal, run rails g migration add_user_id_to_posts.

 $~/KodaCamp> docker-compose exec app bash
 root@0122:/usr/src/app# rails g migration add_user_id_to_posts
       invoke  active_record
       create  db/migrate/xxxxxxxxxxxxxx_add_user_id_to_posts.rb

Let’s reference the users to the posts table.

# db/migrate/xxxxxxxxxxxxxx_add_user_id_to_posts.rb

class AddUserIdToPosts < ActiveRecord::Migration[7.0]
  def change
    add_reference :posts, :user
  end
end

Then run rails db:migrate.

 root@0122:/usr/src/app# rails db:migrate
== xxxxxxxxxxxxxx AddUserIdToPosts: migrating =================================
-- add_reference(:posts, :user)
-> 0.0277s
== xxxxxxxxxxxxxx AddUserIdToPosts: migrated (0.0281s) ========================

Next, set up the relationship of our models.

# app/models/user.rb

class User < ApplicationRecord
  # ...
+ has_many :posts
end
# app/models/post.rb

class Post < ApplicationRecord
  # ...
  has_many :categories, through: :post_category_ships
+ belongs_to :user

  def destroy
    update(deleted_at: Time.now)
  end
end

In PostsController#create, add this line @post.user = current_user

# app/controllers/posts_controller.rb

class PostsController < ApplicationController
  # ...
  def create
    @post = Post.new(post_params)
+   @post.user = current_user
    if @post.save
      flash[:notice] = "Post created successfully"
      redirect_to posts_path
    else
      flash.now[:alert] = "Post create failed"
      render :new, status: :unprocessable_entity
    end
  end
  # ...
end

We added this line @post.user = current_user before calling the save method. In this part, when creating a new post it captures the current user.

Next we’ll be displaying the users in your views.

In posts index and show page, display the post user email.

<!-- app/views/posts/index.html.erb -->

<!-- ... -->
<table>
  <!-- ... -->
+ <td>user</td>
  <td>action</td>
  </thead>
  <% @posts.each do |post| %>
    <tr>
      <td><%= post.title %></td>
      <td><%= post.content %></td>
+     <td><%= post.user&.email %></td>
      <!-- ... -->
    </tr>
  <% end %>
</table>
<!-- ... -->
<!-- app/views/posts/show.html.erb -->

<h1>show post id: <%= @post.id %></h1>
<ul>
  <li><%= @post.title %></li>
  <li><%= @post.content %></li>
+ <li><%= @post.user&.email %></li>
</ul>

<%= link_to 'Index', posts_path %>

When you go to posts_path you’ll get an error saying undefined method 'email' for nil:NilClass

Why are we getting this error?

Because our old records on this line post.user.email, the post.user returns nil

To fix this error, we can remove all of our posts in our database with using Post.destroy_all and create a new ones or add & to this line post.user.email

What is this symbol(&) for?

This is called safe navigation. It does not go to the next step of a chaining method if one of the methods returns nil. In this example, it didn’t call the email attribute because the post.user is nil.

post.user&.email

In PostsController, let’s add the before_action :authenticate_user!, except: [:index, :show].

# app/controllers/posts_controller.rb

class PostsController < ApplicationController
+ before_action :authenticate_user!, except: [:index, :show]
  # ...
end

The :authenticate_user! method is one of the devise helper methods. When a user sends a request, it redirects the requests to the sign-in page if they are unauthenticated.

before_action :authenticate_user!, except: [:index, :show]

Add this in the very top of your posts_controller.

Why are we only excluding the index and show actions?

Because if you don’t exclude the index and show actions, some of our users will not be able to see our posts.

In posts index page, we’ll be adding conditions that show those buttons only for the owner of the post.

<!-- app/views/posts/index.html.erb -->

  <!-- ... -->
  <td>
    <%= link_to 'Show', post_path(post) %>
-   <%= link_to 'Edit', edit_post_path(post) %>
-   <%= button_to 'Delete', post_path(post), method: :delete %>
+   <%= link_to 'Edit', edit_post_path(post) if user_signed_in? && post.user == current_user %>
+   <%= button_to 'Delete', post_path(post), method: :delete if user_signed_in? && post.user == current_user %>
    <!-- ... -->
  </td>
  <!-- ... -->

Our last task will be preventing other users from editing or deleting someone else post, we created this method to check in every request if they are only editing or removing their posts.

In app/controllers/posts_controller.rb, add the method below.

# app/controllers/posts_controller.rb

class PostsController < ApplicationController
  before_action :authenticate_user!, except: [:index, :show]
  before_action :set_post, only: [:show, :edit, :update, :destroy]
+ before_action :validate_post_owner, only: [:edit, :update, :destroy]
  # ...
  private
+
+ def validate_post_owner
+   unless @post.user == current_user
+     flash[:notice] = 'the post not belongs to you'
+     redirect_to posts_path
+   end
+ end
  # ...
end

Since we displayed the user of each post. We must add the user in .includes(:user) to prevent n + 1 problems.

# app/controllers/posts_controller.rb

class PostsController < ApplicationController
  # ...
  def index
-   @posts = Post.includes(:categories).all
+   @posts = Post.includes(:categories, :user).all
  end
end

Reference users to comments

Since we already reference the users to posts. There will be no different when we do it for comments.

Our tasks are:

  • Preventing those unauthenticated users from creating, editing, and deleting comments.
  • Allow authenticated users to create new comments.
  • Only the author of the comment can edit or delete their comments.

In your terminal, run rails g migration add_user_id_to_comments.

 $~/KodaCamp> docker-compose exec app bash
 root@0122:/usr/src/app# rails g migration add_user_id_to_comments
      invoke  active_record
      create  db/migrate/xxxxxxxxxxxxxx_add_user_id_to_comments.rb

Next edit the migration file to reference the users to the posts table.

# db/migrate/xxxxxxxxxxxxxx_add_user_id_to_comments.rb

class AddUserIdToComments < ActiveRecord::Migration[7.0]
  def change
+   add_reference :comments, :user
  end
end

Then run rails db:migrate.

 root@0122:/usr/src/app# rails db:migrate
== xxxxxxxxxxxxxx AddUserIdToComments: migrating =================================
-- add_reference(:posts, :user)
-> 0.0233s
== xxxxxxxxxxxxxx AddUserIdToComments: migrated (0.0279s) ========================

Don’t forget to set up the relationship of our models.

# app/models/user.rb

class User < ApplicationRecord
  # ...
  has_many :posts
+ has_many :comments
end
# app/models/comment.rb

class Comment < ApplicationRecord
  belongs_to :post
+ belongs_to :user

  validates :content, presence: true
end

Follow this set up for our comments_controller since this is the same with posts_controller.

# app/controllers/comments_controller.rb

class CommentsController < ApplicationController
+ before_action :authenticate_user!, except: :index
  before_action :set_post
  before_action :set_comment, only: [:edit, :update, :destroy]
+ before_action :validate_comment_owner, only: [:edit, :update, :destroy]

  def index
-   @comments = @post.comments
+   @comments = @post.comments.includes(:user)
  end
  # ...
  def create
    @comment = @post.comments.build(comment_params)
+   @comment.user = current_user
    if @comment.save
      flash[:notice] = 'Comment created successfully'
      redirect_to post_comments_path(@post)
    else
      render :new
    end
  end
  # ...
  private
  # ...
+
+ def validate_comment_owner
+   unless @comment.user == current_user
+     flash[:notice] = 'the comment not belongs to you'
+     redirect_to post_comments_path(@post)
+   end
+ end
end

The last thing we need add is displaying user’s email in every comment on our comments index page.

<!-- app/views/comments/index.html.erb -->

  <!-- ... -->
  <li>
-   <%= comment.content %>(<%= comment.created_at %>)
-   <%= link_to 'Edit', edit_post_comment_path(@post, comment) %>
-   <%= button_to 'Delete', post_comment_path(@post, comment), method: :delete %>
+   <%= comment.content %>(<%= comment.created_at %>) by <%= comment.user&.email %> 
+   <%= link_to 'Edit', edit_post_comment_path(@post, comment) if user_signed_in? && comment.user == current_user %>
+   <%= button_to 'Delete', post_comment_path(@post, comment), method: :delete if user_signed_in? && comment.user == current_user %>
  </li>
  <!-- ... -->

Now we already integrated the authenticated user in our application.


Back to top

Copyright © 2020-2022 Secure Smarter Service, Inc. This site is powered by KodaCamp.