Carrying on from my last tutorial about building the relationships and core view elements for discussions and posts, we’re going to extend our budding forum by adding users and authentication.
To start, open Gemfile and down the bottom add gem 'devise'
then run bundle install
. Devise is an all-in-one authentication system that handles registration, sessions, authentication, recovering accounts, etc. To set up devise (our authentication station), run rails g devise:install
. We’ll need to set the config.action_mailer.default_url_options option so open config/environments/development.rb and before the end
keyword add config.action_mailer.default_url_options = { :host => 'localhost:3000' }
.
To create the user model which utilises the features of the devise install, run the command rails g devise user
. You’ll also need to run rake db:migrate
afterwards. Devise will handle the routes (routes are the way Rails handles directing incoming traffic that’s destined for different controllers) and code for creating accounts, logging in/out and so forth and by now (if you’ve restarted your Rails server) you should be able to create an account at http://localhost:3000/users/sign_up. If you do create an account, you’ll soon notice it tries to access the root path, so open up config/routes.rb and add root 'discussions#index'
under the resources listed. This turns the root path (http://localhost:3000/ in our case) into a view of all the discussions.
In /app/views/layouts/application.html.erb, underneath the opening body
tag add the following code:
<div>
<% if user_signed_in? %>
Logged in as <strong><%= current_user.email %></strong>.
<%= link_to 'Edit profile', edit_user_registration_path %> |
<%= link_to "Logout", destroy_user_session_path, method: :delete %>
<% else %>
<%= link_to "Sign up", new_user_registration_path %> |
<%= link_to "Login", new_user_session_path %>
<% end %>
</div>
This code checks if a user is signed in and if so, shows who is logged in and provides links to edit their profile or logout. If no one is logged in, it shows sign up/login links.
At the top of both app/controllers/discussions_controller.rb and app/controllers/posts_controller.rb, underneath the class head but above the before_action
line add the following which prevents people from creating/editing/deleting discussions or posts unless they are logged in:
before_filter :authenticate_user!, :only => [:new, :edit, :create, :update, :destroy]
We’ll also need to add associations/references between posts, discussions and users. Run the following commands from the prompt:
$ rails generate migration add_user_to_posts user_id:integer
$ rails generate migration add_user_to_discussions user_id:integer
$ rake db:migrate
Add the following lines before the end
keyword in app/models/user.rb:
has_many :posts
has_many :discussions
And in both app/models/discussion.rb and app/models/post.rb add belongs_to :user
before the end
keyword. This links all our models up quite nicely.
In app/views/posts/_form.html.erb and app/views/discussions/_form.html.erb we’ll want to add <%= f.hidden_field :user_id, value: current_user.id %>
before the submit button. This provides the receiving controller method with the user id of the currently logged in user. In the discussions controller, add @post.user_id = @discussion.user_id
under @post.discussion_id = @discussion.id
in the create
method.
Still in the discussions controller, modify the edit
method so it looks like so:
if current_user == User.find(@discussion.user_id)
else
redirect_to ideas_path
end
And then change the update and destroy methods to look like this:
# PATCH/PUT /discussions/1
def update
if current_user == User.find(@discussion.user_id)
@post = @discussion.posts.first
if @discussion.update(discussion_params)
if @post.update(post_params_on_discussion)
redirect_to @discussion, notice: 'Discussion was successfully updated.'
else
render :edit
end
else
render :edit
end
else
redirect_to ideas_path
end
end
# DELETE /discussions/1
def destroy
if current_user == User.find(@discussion.user_id)
@discussion.destroy
redirect_to discussions_url, notice: 'Discussion was successfully destroyed.'
else
redirect_to ideas_url
end
end
These checks help enforce some integrity, making it so that only the correctly logged in user (or an administrator) can edit their own posts/discussions.
In the posts controller (app/controllers/posts_controller.rb), do some similar magic to your edit
, update
, destroy
methods so they look like such:
# GET /posts/1/edit
def edit
if current_user == User.find(@post.user_id)
else
redirect_to ideas_path
end
end
# PATCH/PUT /posts/1
# PATCH/PUT /posts/1.json
def update
if current_user == User.find(@post.user_id)
if @post.update(post_params)
redirect_to discussion_url(@post.discussion_id), notice: 'Post was successfully updated.'
else
render :edit
end
else
redirect_to discussion_url(@post.discussion_id)
end
end
# DELETE /posts/1
# DELETE /posts/1.json
def destroy
if current_user == User.find(@post.user_id)
@post.destroy
redirect_to discussions_url, notice: 'Post was successfully destroyed.'
else
redirect_to discussions_url
end
end
We’re nearing the end, next you’ll need to edit the discussion_params
and post_params
private methods in the discussions and posts controllers respectively. They should end up looking like so:
def discussion_params
params.require(:discussion).permit(:title, :user_id)
end
def post_params
params.require(:post).permit(:content, :user_id)
end
This allows the user_id
parameter from our edit/new forms to come through to the update
/create
methods and be used as a property of them.
Well, how’s that feel? We’ve now added user authentication and some basic permissions/ownership to our forum!If you want to take it further, check out Part 3 for adding username authentication and Gravatar support.