Carrying on from [my last tutorial]({{< relref "/dev/rails-forum-skeleton-part-1.md" >}}) 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:
<% if user_signed_in? %> Logged in as <%= current_user.email %>. <%= 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 %>
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](http://ghost.adamogrady.id.au/rails-forum-skeleton-part-3/) for adding username authentication and Gravatar support.