Skip to content

Latest commit

 

History

History
576 lines (431 loc) · 15.1 KB

README.md

File metadata and controls

576 lines (431 loc) · 15.1 KB

Session 3 - Controllers and Routing

We now take a closer look at Controller of the MVC architecture and talk about controllers, routing, authentication and authorization.

The controller layer is responsible for interacting with the Model layer and deciding the appropriate presentation. The controllers are in app/controllers directory of Rails application.

We will build a simple blogging website, Blogspot (similar to Medium).

The exercise is difficult and long. Walk through the steps one at a time, understand what each step tries to do and implement it.

Pre-requistes

Routing

The Rails router config/routes.rb recognizes URLs and redirects them to appropriate controller and action. In the first session, we redirected https://localhost:3000 to PagesController and root action.

Rails.application.routes.draw do
  get '/', to: 'page#root'
  get 'about_me', to: 'page#about_me'

We can add routes related to our models (called resources in routing terminology) using the keyword resources. This defines five essential actions:

  • Create one record with new and create.
  • Read one record with show.
  • Update one record with edit and update.
  • Delete one record with destroy.
  • Read all records with index.

For example:

Rails.application.routes.draw do
  resources :articles
end

We can now:

  • See all articles at localhost:3000/articles.
  • Read an article at localhost:3000/articles/1
  • Create an article at localhost:3000/articles/new.
  • Update an article at localhost:3000/articles/1/edit.
  • Delete an article at localhost:3000/articles/1.

HTTP Verbs are a part of routing magic. Read What is REST? | Codeacademy and Resource Routing: the Rails Default to learn more.

While the default actions are usually enough, we can add custom actions as well:

Rails.application.routes.draw do
  resources :articles do
    member do
      put 'publish'
    end
  end
end

Here, we have defined a publish action as a special case for update.

Controllers

The controller's jobs include:

  • Work with request parameters (for example: form inputs).
  • Determine how to use models and existing data.
  • Respond to requests.

The functions inside of a controller file are called actions.

A controller typically has the following actions:

  • create
  • new
  • show
  • index
  • edit
  • update
  • destroy

Often, we want to load some variables or run some checks before an action is executed. For example, we might want to check if the user is logged in or has appropriate permissions before responding with a page.

In such cases, we can use a before_action as follows:

class ArticlesController < ApplicationController::Base
  # Check if the user is logged in before every request.
  before_action :require_login

  ...

  def require_login
    # Send to login page if not logged in
  end
end

We can additionally run the before_action on specific actions using only and except keywords.

Authentication

Authentication is the process by which users can log in and out of their accounts. For example, we might want to restrict access to certain articles only to registered users.

Log in and Log out

We will be implementing Authentication from scratch, although we recommend using the gem devise once you are familiar with Rails and authentication mechanisms.

Authorization

Authorization restricts what resources a given user is allowed to access. For example: we might allow users to delete their own articles as well as adminstrators to delete any article.

We will be using CanCanCan, an authorization gem to keep the process simple.

Blogspot

Blogspot is a blogging website with the following features:

  • Users can register and log into their accounts.
  • Users can create, read, update and delete their own articles.
  • Unregistered users can read only public articles but registered users can read both public and private articles.
  • Adminstrators can delete any article.

Steps

Create Scaffolding for Articles

The Article table has:

  • A string column for the title of the article.
  • A string column for the topic of the article.
  • A string column for the tags of the article.
  • A text column for the content of the article.

Since we want model, controllers and views for the Article class, we will use rails scaffold to get started.

rails generate scaffold Article title:string tags:string topic:string content:text

The above command generates the following important files:

  • db/migrate/<YYYYMMDDHHMMSS>_create_articles.rb, app/models/articles.rb are the migration and model file respectively and have been covered in Session 2 - Model.
  • app/controllers/articles_controller: The controller for Articles - defines how to present views.
  • app/views/articles/*: The view files for Articles.
  • config/routes.rb: Describes the URLs that can be accessed on the application.

After running the migration and opening http://localhost:3000/articles, you should see something like:

Articles Index

  • Let's redirect root url (localhost:3000) to the articles index page as well instead of Welcome page by editing config/routes.rb.
Rails.application.routes.draw do
  root to: 'articles#index'

  resources :articles
end

At this point, we are allowing unregistered users to create and read private articles. Let's fix that by adding authentication.

Add Authentication

The steps here follow from the excellent tutorial, Authentication from Scratch with Rails 5.2 but modified for our project.

  • Install bcrypt to encrypt and keep passwords secure by adding the following line to Gemfile and running bundle install
gem 'bcrypt'
  • Create a User scaffold (as we need models, controller and views). We are adding a user table with a string column for name and e-mail, and storing passwords in encrypted form. The e-mails must be unique as well.
rails generate scaffold User name:string email:uniq password:digest

After running the migration and opening http://localhost:3000/users, you should see something like:

Users Index

You can create a new user by clicking on New User and filling out details:

New User

With this, User registration and resetting password are set up. But we still need pages from which User can log into their account.

  • Create a sessions controller with three actions - new, create and destroy.
rails generate controller sessions new create destroy
  • We put the following code into app/controllers/sessions_controller.rb:
class SessionsController < ApplicationController
  def new
  end

  def create
    user = User.find_by_email(params[:email)
    if user && user.authenticate(params[:password])
      session[:user_id] = user.id
      redirect_to root_url, notce: 'Logged in!'
    else
      flash[:alert] = 'Email or password is invalid'
      render 'new'
    end
  end

  def destroy
    session[:user_id] = nil
    redirect_to root_url, notice: 'Logged out!'
  end
end

As you can see we are using session[:user_id] to store the logged in user id.

  • We modify the routes files to allow requests for the SessionsController:
Rails.application.routes.draw do
  root to: 'articles#index'

  resources :articles
  resources :users

  resources :sessions, only: [:new, :create, :destroy]
end

Note: Despite not having a sessions table or model, we are effectively treating it as resource with create and destroy operations.

  • We create the login page at app/views/sessions/new.html.erb:
<p id="alert"><%= alert %></p>

<h1>Login</h1>

<%= form_with url: sessions_path do |f| %>
  <div class="field">
    <%= f.label :email %>
    <%= f.email_field :email %>
  </div>

  <div class="field">
    <%= f.label :password %>
    <%= f.password_field :password %>
  </div>

  <div class="actions">
     <%= f.submit 'Login' %>
  </div>
<% end %>

Login Page

  • While everything works, remembering localhost:3000/sessions/new is hard to do. Let's add easy to remember routes:
Rails.application.routes.draw do
  root to: 'articles#index'

  resources :articles
  resources :users

  resources :sessions, only: [:new, :create, :destroy]

  get 'signup', to: 'users#new', as: 'signup'
  get 'login', to: 'sessions#new', as: 'login'
  delete 'logout', to: 'sessions#destroy', as: 'logout'
end
  • Log in and log out process works as expected but it's not clear whether we are logged in by looking at the page. Let's add a navigation bar:

app/controllers/application_controller.rb:

class ApplicationController < ActionController::Base
  helper_method :current_user

  def current_user
    if session[:user_id]
      @current_user ||= User.find(session[:user_id])
    else
      @current_user = nil
    end
  end
end

app/views/layout/application.html.erb:

<!DOCTYPE html>
<html>
  ...
  <body>
    <% if current_user %>
      Logged in as <%= current_user.email %>
      <%= link_to 'Log Out', logout_path %>
    <% else %>
      <%= link_to 'Sign Up', signup_path %>
      <%= link_to 'Log In', login_path, method: :delete %>
    <% end %>
  </body>

  <%= yield %>
</html>

Article Index with Navbar

Authentication is now completed!

Modifying Articles

We will add two new columns to the articles:

  • An integer column user_id to store the user id of author of the article.
  • An boolean column public to store whether the article can be read by unregistered users.

These two columns are important for us to identify whether a user can read, update, destroy an article or not.

  • Add a migration to add new columns and migrate.
rails generate migration add_authorization_colums_to_articles user_id:integer public:boolean
  • Add a radio button in app/views/articles/_form.html.erb for public attribute.
<%= form_with(model: article) do |form| %>
  <% if article.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(article.errors.count, "error") %> prohibited this article from being saved:</h2>

      <ul>
        <% article.errors.each do |error| %>
          <li><%= error.full_message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= form.label :title %>
    <%= form.text_field :title %>
  </div>

  <div class="field">
    <%= form.label :topic %>
    <%= form.text_field :topic %>
  </div>

  <div class="field">
    <%= form.label :tags %>
    <%= form.text_field :tags %>
  </div>

  <div class="field">
    <%= form.label :content %>
    <%= form.text_area :content %>
  </div>

  <div class="field">
    <%= form.label :public %>
    <%= form.check_box :public %>
  </div>

  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>
  • Permit values assigned to public attribute to used for mass assignment by updating article_params function in app/controllers/articles_controller.rb.
def article_params
  params.require(:article).permit(:title, :topic, :tags, :content, :public)
end
  • Assign user id to a new Article when it is being created by updating create function in app/controllers/articles_controller.rb
def create
  @article = Article.new(article_params)
  @article.user_id = current_user.id

  if @article.save
    redirect_to @article, notice: 'Article was successfully created.'
  else
    render :new
  end
end

Adding Authorization

We will be using CanCanCan, an authorization library to simplify the adding authorization.

  • Install CanCanCan by adding the following line to Gemfile and running bundle install.
gem 'cancancan'
  • Generate an Ability class - CanCanCan calls the actions a user can take abilities.
rails g cancan:ability
  • Edit app/models/ability.rb as follows:
class Ability
  include CanCan::Ability

  def initialize(user)
    # All users
    can :index, Article
    
    # Can read public articles
    can :show, Article, public: true

    # Additional permissions for logged in users
    if user.present?
      # Can read private articles
      can :show, Article, public: false

      # Can create articles
      can :new, Article
      can :create, Article

      # Can edit their own articles
      can :edit, Article, user_id: user.id
      can :update, Article, user_id: user.id

      # Can destroy their own articles
      can :destroy, Article, user_id: user.id
    end
  end
end

We have defined the different abilities different type of users have.

  • Add load_and_authorize_resource to ArticlesController to check every request that user makes.
class ArticlesController < ApplicationController
  load_and_authorize_resource

  ...
end

Authorization is complete!

However, at this point we still have useless links which the user can still click and be redirected back to root url.

We can check the current user's permission using:

<% if can? :create, Article %>
  <%= link_to 'New Article', new_article_path %>
<% end %>

Once done, you can submit your code changes and create a pull request. There are no automated tests for the session although you can add screenshots of the website in the pull request description.

Bonus Tasks

  • Extend the authorization in a similar sense to UsersController to prevent users from changing each other's profile information and passwords.

  • Certain users (called admins) can delete articles created by their users as well, update lost passwords and other administrative tasks.

    Try implementing that by:

    • Adding a boolean column to users table called admin.
    • Adding additional abilities if the user.admin is true.
  • Some popular websites like to limit the number of "private" articles an authenticated user can read to 3 or 5. You can implement the same feature by:

    • Adding an attribute private_articles_remaining to the Users model.
    • Store the number of private_articles_remaining in sessions variable and decrement it on each private article read.
    • Pass down the number of remaining articles to the ability check.