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
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 thedevise
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 preventn + 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.