The Simplified Go Food Web App - Iteration 5

Previously on Our App...

In the last lesson you have learned to:

  1.  

User and Authentication

The Requirements

It's your product manager again! As usual, he comes up with more requests and requirements. This time around he wants you to create a User model that satisfies the following rules:

  1. User should have a username and a password
  2. User can not have duplicate username
  3. User's password should be at least 8 characters long
  4. When entering a password, User should enter a matching password confirmation

Write The Specs

You know the drill, try writing the specs.

User Model Spec (1)

First, we only define specs for username. Specs for password will be a little bit different.

require 'rails_helper'

describe User do
  it "has a valid factory" do
    expect(build(:user)).to be_valid
  end

  it "is valid with a username" do
    expect(build(:user)).to be_valid
  end

  it "is invalid without a username" do
    user = build(:user, username: nil)
    user.valid?
    expect(user.errors[:username]).to include("can't be blank")
  end

  it "is invalid with duplicate username" do
    user1 = create(:user, username: "User 1")
    user2 = build(:user, username: "User 1")
    
    user2.valid?
    expect(user2.errors[:username]).to include("has already been taken")
  end
  # -cut-
end

User Model Spec (2)

For password, remember that we always prompt users with two fields: password and password_confirmation.

describe User do
  # -cut-
  context "on a new user" do
    it "is invalid without a password" do
      user = build(:user, password: nil, password_confirmation: nil)
      user.valid?
      expect(user.errors[:password]).to include("can't be blank")
    end

    it "is invalid with less than 8 characters password" do
      user = build(:user, password: "short", password_confirmation: "short")
      user.valid?
      expect(user.errors[:password]).to include("is too short (minimum is 8 characters)")
    end

    it "is invalid with a confirmation mismatch" do
      user = FactoryGirl.build(:user, password: "longpassword", password_confirmation: "longpassword1")
      user.valid?
      expect(user.errors[:password_confirmation]).to include("doesn't match Password")
    end
  end
  # -cut-
end

User Model Spec (3)

When updating an existing user, don't check for password's length again because it's already tested.

describe User do
  # -cut-
  context "on an existing user" do
    before :each do
      @user = create(:user)
    end

    it "is valid with no changes" do
      expect(@user.valid?).to eq(true)
    end

    it "is invalid with an empty password" do
      @user.password_digest = ""
      @user.valid?
      expect(@user.errors[:password]).to include("can't be blank")
    end

    it "is valid with a new (valid) password" do
      @user.password = "newlongpassword"
      @user.password_confirmation = "newlongpassword"
      expect(@user.valid?).to eq(true)
    end
  end
end

User Factory

For User factory, we don't use Faker to generate password to ensure the password_confirmation matches the password.

FactoryGirl.define do
  factory :user do
    username { Faker::Internet.unique.user_name }
    password "longpassword"
    password_confirmation "longpassword"
  end
end

Creating User Model

Here, you will also learn new Rails features.

rails generate model User username:string password:digest

When you define a password as a digest type, Rails will equip it with ActiveModel's method called has_secure_password like this:

class User < ApplicationRecord
  has_secure_password
end

Run rails db:migrate.

Has Secure Password

When you finished running rails db:migrate, you will notice that your schema.rb will look like this.

# -cut-  
  create_table "users", force: :cascade do |t|
    t.string   "username"
    t.string   "password_digest"
    t.datetime "created_at",      null: false
    t.datetime "updated_at",      null: false
  end
# -cut-

ActiveModel's has_secure_password makes sure that all passwords stored in "password_digest" is stored in encrypted format rather than plain password. It also ensures that we can pass "password" and "password_confirmation" in User model and controller.

Bcrypt

To make use password digest to the fullest, we need to install Bcrypt gem. Simply uncomment this line in your Gemfile.

gem 'bcrypt', '~> 3.1.7'

Then run bundle install.

Adding Validations to User

Then we will add validations to our User model.

class User < ApplicationRecord
  has_secure_password

  validates :username, presence: true, uniqueness: true
  validates :password, presence: true, on: :create
  validates :password, length: { minimum: 8 }, allow_blank: true
end

ActiveModel's has_secure password already handles several validations for our password field. We just need to validate that password should be present on creation and its length should be 8 characters minimum. Allowing blank is somehow needed to make it works when we updating a User instance without modifying its password at all.

You Shall Pass

With this set up, you should pass your model specs. Don't forget to commit your progress so far.

Users Controller Specs (1)

Next, we will write our Users controller specs. Our Users controller will be a very simple CRUD. The only difference is when testing for update method, there is a specific way to test for password changes. You will be guided writing test for update, but by now you should be able to write the rest of the specs by yourself.

Users Controller Specs (2)

# -cut-
describe UsersController do
  describe 'GET #index' do
    it "populates an array of all users" do 
      user1 = create(:user, username: "user1")
      user2 = create(:user, username: "user2")
      get :index
      expect(assigns(:users)).to match_array([user1, user2])
    end

    it "renders the :index template" do
      get :index
      expect(response).to render_template :index
    end
  end

  describe 'GET #show' do
    it "assigns the requested user to @user" do
      user = create(:user)
      get :show, params: { id: user }
      expect(assigns(:user)).to eq user
    end

    it "renders the :show template" do
      user = create(:user)
      get :show, params: { id: user }
      expect(response).to render_template :show
    end
  end
  # -cut-
end

Users Controller Specs (3)

describe UsersController do
  # -cut-
  describe 'GET #new' do
    it "assigns a new User to @user" do
      get :new
      expect(assigns(:user)).to be_a_new(User)
    end

    it "renders the :new template" do
      get :new
      expect(:response).to render_template :new
    end
  end

  describe 'GET #edit' do
    it "assigns the requested user to @user" do
      user = create(:user)
      get :edit, params: { id: user }
      expect(assigns(:user)).to eq user
    end

    it "renders the :edit template" do
      user = create(:user)
      get :edit, params: { id: user }
      expect(response).to render_template :edit
    end
  end
  # -cut-
end

Users Controller Specs (4)

describe UsersController do
  # -cut-
  describe 'POST #create' do
    context "with valid attributes" do
      it "saves the new user in the database" do
        expect{
          post :create, params: { user: attributes_for(:user) }
        }.to change(User, :count).by(1)
      end

      it "redirects to users#index" do
        post :create, params: { user: attributes_for(:user) }
        expect(response).to redirect_to users_url
      end
    end

    context "with invalid attributes" do
      it "does not save the new user in the database" do
        expect{
          post :create, params: { user: attributes_for(:invalid_user) }
        }.not_to change(User, :count)
      end

      it "re-renders the :new template" do
        post :create, params: { user: attributes_for(:invalid_user) }
        expect(response).to render_template :new
      end
    end
  end
  # -cut-
end

Users Controller Specs (5)

describe UsersController do
  # -cut-
  describe 'PATCH #update' do
    before :each do
      @user = create(:user, password: 'oldpassword', password_confirmation: 'oldpassword')
    end
    # -cut-
  end
  # -cut-
end

For update, first we create an object @user with a password and a password_confirmation.

Users Controller Specs (6)

describe UsersController do
  # -cut-
  describe 'PATCH #update' do
    # -cut-
    context "with valid attributes" do
      it "locates the requested @user" do
        patch :update, params: { id: @user, user: attributes_for(:user) }
        expect(assigns(:user)).to eq @user
      end

      it "saves new password" do
        patch :update, params: { id: @user, user: attributes_for(:user, password: 'newlongpassword', password_confirmation: 'newlongpassword') }
        @user.reload
        expect(@user.authenticate('newlongpassword')).to eq(@user)
      end

      it "redirects to users#index" do
        patch :update, params: { id: @user, user: attributes_for(:user) }
        expect(response).to redirect_to users_url
      end

      it "disables login with old password" do
        patch :update, params: { id: @user, user: attributes_for(:user, password: 'newlongpassword', password_confirmation: 'newlongpassword') }
        @user.reload
        expect(@user.authenticate('oldpassword')).to eq(false)
      end
    end
    # -cut-
  end
  # -cut-
end

Users Controller Specs (7)

describe UsersController do
  # -cut-
  describe 'PATCH #update' do
    # -cut-
    context "with invalid attributes" do
      it "does not update the user in the database" do
        patch :update, params: { id: @user, user: attributes_for(:user, password: nil, password_confirmation: nil) }
        @user.reload
        expect(@user.authenticate(nil)).to eq(false)
      end

      it "re-renders the :edit template" do
        patch :update, params: { id: @user, user: attributes_for(:invalid_user) }
        expect(response).to render_template :edit
      end
    end
  end
  # -cut-
end

As you can see here and in the previous slide, we test password changes using "@user.authenticate" method instead of testing the value of @user.password.

Users Controller Specs (8)

describe UsersController do
  # -cut-
  describe 'DELETE #destroy' do
    before :each do
      @user = create(:user)
    end

    it "deletes the user from the database" do
      expect{
        delete :destroy, params: { id: @user }
      }.to change(User, :count).by(-1)
    end

    it "redirects to users#index" do
      delete :destroy, params: { id: @user }
      expect(response).to redirect_to users_url
    end
  end
end

Users Controller (1)

Now try to write the Users controller. This is a standard CRUD controller, you should be able to write it just fine.

Users Controller (2)

class UsersController < ApplicationController
  before_action :set_user, only: [:show, :edit, :update, :destroy]

  # -cut-

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_user
      @user = User.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def user_params
      params.require(:user).permit(:username, :password, :password_confirmation)
    end
end

Make sure that we allow parameters password and password_confirmation in our Users controller.

Users Controller (3)

class UsersController < ApplicationController
  before_action :set_user, only: [:show, :edit, :update, :destroy]

  # GET /users
  # GET /users.json
  def index
    @users = User.all
  end

  # GET /users/1
  # GET /users/1.json
  def show
  end

  # GET /users/new
  def new
    @user = User.new
  end

  # GET /users/1/edit
  def edit
  end
  # -cut-
end

Users Controller (4)

class UsersController < ApplicationController
  # -cut-
  # POST /users
  # POST /users.json
  def create
    @user = User.new(user_params)

    respond_to do |format|
      if @user.save
        format.html { redirect_to users_url, notice: 'User was successfully created.' }
        format.json { render :show, status: :created, location: @user }
      else
        format.html { render :new }
        format.json { render json: @user.errors, status: :unprocessable_entity }
      end
    end
  end
  # -cut-
end

Please note that we change create method to redirect to Users index page when it is finished.

Users Controller (5)

class UsersController < ApplicationController
  # -cut-
  # PATCH/PUT /users/1
  # PATCH/PUT /users/1.json
  def update
    respond_to do |format|
      if @user.update(user_params)
        format.html { redirect_to users_url, notice: 'User was successfully updated.' }
        format.json { render :show, status: :ok, location: @user }

        @users = User.all
      else
        format.html { render :edit }
        format.json { render json: @user.errors, status: :unprocessable_entity }
      end
    end
  end
  # -cut-
end

We also change update method to redirect to Users index page when it is finished.

Users Controller (6)

class UsersController < ApplicationController
  # -cut-
  # DELETE /users/1
  # DELETE /users/1.json
  def destroy
    @user.destroy
    respond_to do |format|
      format.html { redirect_to users_url, notice: 'User was successfully destroyed.' }
      format.json { head :no_content }
    end
  end
  # -cut-
end

Routes

Rails.application.routes.draw do
  get 'home/hello'
  root 'store#index', as: 'store_index'  
  resources :carts
  resources :foods
  resources :line_items
  resources :orders
  resources :users
end

Don't forget to add users routes.

The Views (1)

Next, try to write the views. Just as the controller, there is not much different in user views than the regular views. The only difference will be in the form.

The Views (2)

In our User form, we use "password_field" for the first time.

<%= form_for(user) do |f| %>
  <% if user.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(user.errors.count, "error") %> prohibited this user from being saved:</h2>

      <ul>
      <% user.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <fieldset>
    <legend>Enter User Details</legend>

    <div class="field">
      <%= f.label :username %>
      <%= f.text_field :username, size: 40 %>
    </div>

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

    <div class="field">
      <%= f.label :password_confirmation, 'Confirm:' %>
      <%= f.password_field :password_confirmation %>
    </div>

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

The Views (3)

Everything else is just a standard CRUD view.

json.extract! user, :id, :name, :description, :image_url, :price, :created_at, :updated_at
json.url user_url(user, format: :json)

_user.json.jbuilder

<h1>Editing User</h1>

<%= render 'form', user: @user %>

<%= link_to 'Show', @user %> |
<%= link_to 'Back', users_path %>

edit.html.erb

The Views (4)

<h1>New User</h1>

<%= render 'form', user: @user %>

<%= link_to 'Back', users_path %>

new.html.erb

<p id="notice"><%= notice %></p>

<p>
  <strong>Username:</strong>
  <%= @user.username %>
</p>

<%= link_to 'Edit', edit_user_path(@user) %> |
<%= link_to 'Back', users_path %>

show.html.erb

The Views (5)

<p id="notice"><%= notice %></p>

<h1>Users</h1>

<table>
  <% @users.each do |user| %>
    <tr class="<%= cycle('list_line_odd', 'list_line_even') %>">
      <td><%= user.username %></td>
      <td>
        <%= link_to 'Show', user %>  
        <%= link_to 'Edit', edit_user_path(user) %>  
        <%= link_to 'Destroy', user, method: :delete, data: { confirm: 'Are you sure?' } %>
      </td>
    </tr>
  <% end %>
</table>

<br/>

<%= link_to 'New User', new_user_path %>

index.html.erb

The Views (6)

json.array! @users, partial: 'users/user', as: :user

index.json.jbuilder

json.partial! "users/user", user: @user

show.json.jbuilder

You Shall Pass

With this set up, you should pass your Users controller specs. Try adding, editing, and deleting users. Let at least one user remains so you can use it for login and logout later.

Don't forget to commit your progress so far.

Sessions

We have created our User MVC. Next, we are going to use it for authentication. For this, we will create a new controller called Sessions controller.

Sessions controller only has three methods:

  1. New method is the one we use to load the login form
  2. Create method is the one that actually does the login action and store user_id to session variable
  3. Destroy method is the one we use to logout and remove user_id from session variable

Sessions Controller Specs (1)

Since its unique nature, you will be guided to write Sessions controller specs.

require 'rails_helper'

describe SessionsController do
  describe "GET new" do
    it "renders the :new template" do
      get :new
      expect(:response).to render_template :new
    end
  end
  # -cut-
end

First, we only define new method to render the login template (:new session).

Sessions Controller Specs (2)

describe SessionsController do
  # -cut-
  describe "POST create" do
    before :each do
      @user = create(:user, username: 'user1', password: 'longpassword', password_confirmation: 'longpassword')
    end

    context "with valid username and password" do
      it "assigns user_id to session variables" do
        post :create, params: { username: 'user1', password: 'longpassword' }
        expect(session[:user_id]).to eq(@user.id)
      end

      it "redirects to admin index page" do
        post :create, params: { username: 'user1', password: 'longpassword' }
        expect(response).to redirect_to admin_url
      end
    end

    context "with invalid username and password" do
      it "redirects to login page" do
        post :create, params: { username: 'user1', password: 'wrongpassword' }
        expect(response).to redirect_to login_url
      end
    end
  end
  # -cut-
end

Sessions Controller Specs (3)

require 'rails_helper'

describe SessionsController do
  # -cut-
  describe "DELETE destroy" do
    before :each do
      @user = create(:user)
    end

    it "removes user_id from session variables" do
      delete :destroy, params: { id: @user }
      expect(session[:user_id]).to eq(nil)
    end

    it "redirects to store index page" do
      delete :destroy, params: { id: @user }
      expect(response).to redirect_to store_index_url
    end
  end
end

Sessions Controller

This is how your Sessions controller should look like.

class SessionsController < ApplicationController
  def new
  end

  def create
    user = User.find_by(username: params[:username])
    if user.try(:authenticate, params[:password])
      session[:user_id] = user.id
      redirect_to admin_url
    else
      redirect_to login_url, alert: "Invalid user/password combination" 
    end
  end

  def destroy
    session[:user_id] = nil
    redirect_to store_index_url, notice: "Logged out"
  end
end

Routes

We are going to use special routes for our Sessions controller.

Rails.application.routes.draw do
  get 'home/hello'
  root 'store#index', as: 'store_index'
  
  controller :sessions do
    get 'login' => :new
    post 'login' => :create
    delete 'logout' => :destroy
  end
  
  resources :carts
  resources :foods
  resources :line_items
  resources :orders
  resources :users
end

Login Form

Although we define three methods for Sessions controller, we only need one view (the login form) in Sessions' new.html.erb.

<div class="go_food_form">
  <% if flash[:alert] %>
    <p id="notice"><%= flash[:alert] %></p>
  <% end %>
  
  <%= form_tag do %>
    <fieldset>
      <legend>Please Log In</legend>
      
      <div>
        <%= label_tag :username, 'Userame:' %>
        <%= text_field_tag :username, params[:username] %>
      </div>
      
      <div>
        <%= label_tag :password, 'Password:' %>
        <%= password_field_tag :password, params[:password] %>
      </div>
      
      <div>
        <%= submit_tag "Login" %>
      </div>
    </fieldset>
  <% end %>
</div>

Admin Controller

To make our Sessions controller spec pass, we need to generate Admin controller.

rails generate controller Admin index

You Shall Pass

With this set up, you should pass your Sessions controller specs. Don't forget to commit your progress so far.

Adding Login/Logout to The Sidebar

We need to make our login/logout button visible from any page in our app. Let's modify our application.html.erb file.

<!DOCTYPE html>
<html>
  <!-- cut -->
  <body class="<%= controller.controller_name %>">
    <!-- cut -->
    <div id="columns">
      <div id="side">
        <!-- cut -->
        <% if session[:user_id] %>
          <ul>
            <li><%= link_to 'Orders', orders_path %></li>
            <li><%= link_to 'Foods', foods_path %></li>
            <li><%= link_to 'Users', users_path %></li>
            <li> </li>
            <li><%= button_to 'Logout', logout_path, method: :delete %></li>
          </ul>
          
        <% else %>
          <ul>
            <li><%= link_to 'Login', login_path %></li>
          </ul>
        <% end %>
      </div>
      <!-- cut -->
    </div>
  </body>
</html>

Limiting Access (1)

Next, we are going to limit access to parts our controllers only for logged in users. First, let's modify our Application controller.

class ApplicationController < ActionController::Base
  before_action :authorize
  protect_from_forgery with: :exception

  protected
    def authorize
      unless User.find_by(id: session[:user_id])
        redirect_to login_url, notice: 'Please Login'
      end
    end
end

This will ensure that every controller (which is the children of Application controller) will check for users' credential and redirect them to login page if they don't pass the check.

Limiting Access (2)

Then, we are going to skip "before_action :authorize" for several parts of our controllers that should be accessible by everyone. Add these lines in their respective files.

class CartsController < ApplicationController
  skip_before_action :authorize, only: [:create, :update, :destroy]
end

class LineItemsController < ApplicationController
  skip_before_action :authorize, only: :create
end

class OrdersController < ApplicationController
  skip_before_action :authorize, only: [:new, :create]
end

class SessionsController < ApplicationController
  skip_before_action :authorize
end

class StoreController < ApplicationController
  skip_before_action :authorize
end

Play Around

Now you can try login and logout and accessing different pages in your app to see if the authorization actually works. Afterward, don't forget to commit your progress.

The Simplified Go Food Web App - Iteration 5

By qblfrb

The Simplified Go Food Web App - Iteration 5

  • 289