Custom Login with Rails Part-2

In the previous part we saw how to build the signup page. In this part we will create sessions and persist our user object in the session. To start this, we will create a controller to handle the sessions.

$ rails g controller sessions new
      create  app/controllers/sessions_controller.rb
       route  get 'sessions/new'
      invoke  erb
      create    app/views/sessions
      create    app/views/sessions/new.html.erb
      invoke  test_unit
      create    test/controllers/sessions_controller_test.rb
      invoke  helper
      create    app/helpers/sessions_helper.rb
      invoke    test_unit
      create      test/helpers/sessions_helper_test.rb
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/sessions.js.coffee
      invoke    scss
      create      app/assets/stylesheets/sessions.css.scss

Modify the routes to create resources for the sessions controller.

config/routes.rb
Rails.application.routes.draw do

  resources :users
  resources :sessions
  root 'home#index'
end

In sessions controller, we will modify our actions to check the user by user_name. In case you want, you can change it to email or both. Rails does some more magic behind the scenes using the user.authenticate(params[:password]). has_secure_password generates the salt and allows us to use .authenticate method which matches the salted password with the params passed. Also, we will persist the user_id in the session. In order to create a logout, we will simply set the session’s user_id to nil.

app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  def new
  end

  def create
    user = User.find_by(user_name: params[:user_name]) rescue nil
    if (user && user.authenticate(params[:password]))
      session[:user_id] = user.id.to_s
      redirect_to dashboards_path
    else
      flash.now.alert = "Invalid Username or Password"
      render "new"
    end
  end

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

Our login form will look like the following and will be accessible at sessions/new.

app/views/sessions/new
<% flash.each do |key, msg| %>
 <%= content_tag :p, msg, :class => [key] %>
<% end %>

<%= form_tag sessions_path do %>
 <label>User Name</label>
 <%= text_field_tag :user_name, params[:user_name] %>

 <label>Password</label>
 <%= password_field_tag :password, nil %>

 <%= submit_tag "Login"%>
<% end %>

We will also create a separate route for logout.

config/routes.rb
Rails.application.routes.draw do

  resources :users
  resources :sessions

  get "logout", to: "sessions#destroy", as: "logout"

  resources :dashboards
  root 'home#index'
end

We will create a blank controller where we can redirect the page after the session is created.

$ rails g controller dashboards index
      create  app/controllers/dashboards_controller.rb
       route  get 'dashboards/index'
      invoke  erb
      create    app/views/dashboards
      create    app/views/dashboards/index.html.erb
      invoke  test_unit
      create    test/controllers/dashboards_controller_test.rb
      invoke  helper
      create    app/helpers/dashboards_helper.rb
      invoke    test_unit
      create      test/helpers/dashboards_helper_test.rb
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/dashboards.js.coffee
      invoke    scss
      create      app/assets/stylesheets/dashboards.css.scss

Now, to access the persisted user as an object in the session we will create a current_user method. Through this we can access the user’s details when the he or she is in the session.

app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception
  helper_method :current_user

  private

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

We also need a filter method to protect our methods that we need to keep only for the logged in Users. In order to do that, we will create a method called authenticate_user. This method will check the presence of user_id and accordingly redirect to the respective page.

app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception
  helper_method :current_user

  private

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

  protected

  def authenticate_user
    if session[:user_id]
      # set current user object to @current_user object variable
      @current_user = User.find(session[:user_id]) rescue nil
      return true
    else
      redirect_to new_session_path
      return false
    end
  end
end

We can now put our dashboard behind the login.

app/controllers/dashboards_controller.rb
class DashboardsController < ApplicationController
  before_action :authenticate_user

  def index
  end
end

Also, we can use current_user object to show conditional login and logout links and also show the details of the session user.

app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
<head>
  <title>Login App</title>
  <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track' => true %>
  <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
  <%= csrf_meta_tags %>
</head>
<body>

<% if current_user.present? %>
  <%= current_user.user_name %> <%= link_to "Logout", logout_path%>
<%else%>
  <%= link_to "Login", new_session_path %>
<% end %>

<%= yield %>

</body>
</html>

So, we have successfully created sessions, made method to protect actions and also persist the user in the session. In the Part-3 of the this tutorial, we will look at confirmation and password recovery.

Create custom login in Rails – Part 1

We will start with creating a model for the user. We need to create a field called password_digest for storing the encrypted password.

$ rails g model user user_name:string password_digest:string
      invoke  mongoid
      create    app/models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml

We need to add ‘bcrypt’ gem to the Gemfile as it is used by rails to encrypt the password internally.

Gemfile
gem 'bcrypt'

ActiveModel::SecurePassword is a module required for generating and validating passwords in rails. In order to enable this module on a particular model, we need to include it. Then access these methods using has_secure_password.

app/models/user.rb
class User
  include Mongoid::Document
  include Mongoid::Timestamps
  include ActiveModel::SecurePassword

  field :user_name, type: String
  field :password_digest, type: String

  has_secure_password
end

We have already added the ability to create users, password and password confirmation to our system. Let’s quickly check what we have done.

$ rails c
Loading development environment (Rails 4.1.1)
2.1.1 :001 > u = User.new
 => #
2.1.1 :002 > u.user_name = "saurabh"
 => "saurabh"
2.1.1 :003 > u.password = "123456"
 => "123456"
2.1.1 :004 > u.password_confirmation = "123456"
 => "123456"
2.1.1 :005 > u.save
  MOPED: 127.0.0.1:27017 COMMAND      database=admin command={:ismaster=>1} runtime: 2.0463ms
  MOPED: 127.0.0.1:27017 INSERT       database=learning_development collection=users documents=[{"_id"=>BSON::ObjectId('53c24217676172180d000000'), "user_name"=>"saurabh", "password_digest"=>"$2a$10$AvOo2g.RD4Sb31xHzspcJe34uzz6tY9roaZHQoYU4nwWZN8GyCe1C", "updated_at"=>2014-07-13 08:24:23 UTC, "created_at"=>2014-07-13 08:24:23 UTC}] flags=[]
                         COMMAND      database=learning_development command={:getlasterror=>1, :w=>1} runtime: 5.5310ms
 => true
2.1.1 :006 > u
 => #

In order to handle the creation of users, we will create a controller.

$ rails g controller users new
      create  app/controllers/users_controller.rb
       route  get 'users/new'
      invoke  erb
      create    app/views/users
      create    app/views/users/new.html.erb
      invoke  test_unit
      create    test/controllers/users_controller_test.rb
      invoke  helper
      create    app/helpers/users_helper.rb
      invoke    test_unit
      create      test/helpers/users_helper_test.rb
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/users.js.coffee
      invoke    scss
      create      app/assets/stylesheets/users.css.scss

We also need to setup the routes for users and create a root page. We will modify the users route to make it resful.

config/routes.rb
Rails.application.routes.draw do
  resources :users
  root 'home#index'
end

Let’s modify the controller and create a new and create method. Make sure you whitelist a limited set of params not permit all.

app/controllers/users_controller.rb
class UsersController < ApplicationController
  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)
      if @user.save
  	redirect_to root_path
  	flash["notice"] = "Signed Up Successfully"
      else
  	render "new"
        flash["error"] = "Problems with your Signup"
      end
  end

  def user_params
     params.require(:user).permit(:password, :password_confirmation, :user_name, :email)
  end
end

In order to create signup for the user, we need to add a form for it.

app/views/users/new.html.erb
<% if flash["notice"].present? -%>
   <%= flash[:notice]%>
<% end -%>

<%= form_for @user do |f|%>

   <label>User Name</label>
   <%= f.text_field :user_name %>

   <label>Email</label>
   <%= f.email_field :email %>

   <label>Password</label>
   <%= f.password_field :password %>

   <label>Password Confirmation</label>
   <%= f.password_field :password_confirmation %>

   <%= f.submit %>
<% end %>

We will create session and session objects in the next part.

Update: You can read the Part 2 of the tutorial here .

Rails engines with Mongoid

In order to create a rails engine that loads mongoid by default instead of activerecord, we will start with creating a full rails engine with an option -O. This is to skip the inclusion of any database settings or active record.

$ rails plugin new new_engine --full -O
      create  
      create  README.rdoc
      create  Rakefile
      create  new_engine.gemspec
      create  MIT-LICENSE
      create  .gitignore
      create  Gemfile
      create  app/models
      create  app/models/.keep
      create  app/controllers
      create  app/controllers/.keep
      create  app/views
      create  app/views/.keep
      create  app/helpers
      create  app/helpers/.keep
      create  app/mailers
      create  app/mailers/.keep
      create  app/assets/images/new_engine
      create  app/assets/images/new_engine/.keep
      create  config/routes.rb
      create  lib/new_engine.rb
      create  lib/tasks/new_engine_tasks.rake
      create  lib/new_engine/version.rb
      create  lib/new_engine/engine.rb
      create  app/assets/stylesheets/new_engine
      create  app/assets/stylesheets/new_engine/.keep
      create  app/assets/javascripts/new_engine
      create  app/assets/javascripts/new_engine/.keep
      create  bin
      create  bin/rails
      create  test/test_helper.rb
      create  test/new_engine_test.rb
      append  Rakefile
      create  test/integration/navigation_test.rb
  vendor_app  test/dummy
         run  bundle install
Fetching gem metadata from https://rubygems.org/...........
Fetching additional metadata from https://rubygems.org/..
Resolving dependencies...
................

Despite this, as rails loads all the railties, it even loads activerecord with it.Hence, we need to customize our list of railties in rails/bin file.

rails/bin
#!/usr/bin/env ruby
# This command will automatically be run when you run "rails" with Rails 4 gems installed from the root of your application.

ENGINE_ROOT = File.expand_path('../..', __FILE__)
ENGINE_PATH = File.expand_path('../../lib/new_engine/engine', __FILE__)

# Set up gems listed in the Gemfile.
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])

require "action_controller/railtie"
require "action_mailer/railtie"
require "sprockets/railtie"
require "rails/test_unit/railtie"
require "mongoid"
require 'rails/engine/commands'

We will now try to create a model inside the engine.

$ rails g model product title:string
bin/rails:15:in `require': cannot load such file -- mongoid (LoadError)
	from bin/rails:15:in `<main>'

In order to load mongoid, we will add it as a dependency to the gem. Make sure you use the same version of mongoid inside your application and rails engine.

new_engine.gemspec
$:.push File.expand_path("../lib", __FILE__)

# Maintain your gem's version:
require "new_engine/version"

# Describe your gem and declare its dependencies:
Gem::Specification.new do |s|
  s.name        = "new_engine"
  s.version     = NewEngine::VERSION
  s.authors     = ["TODO: Your name"]
  s.email       = ["TODO: Your email"]
  s.homepage    = "TODO"
  s.summary     = "TODO: Summary of NewEngine."
  s.description = "TODO: Description of NewEngine."
  s.license     = "MIT"

  s.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.rdoc"]
  s.test_files = Dir["test/**/*"]

  s.add_dependency "rails", "~> 4.1.1"
  s.add_dependency 'mongoid', '4.0.0'
end

At this point we will run bundle install again and try to create a model.


$ rails g model product title:string
      invoke  mongoid
      create    app/models/product.rb
      invoke    test_unit
      create      test/models/product_test.rb
      create      test/fixtures/products.yml

Voila! You can now develop your engine using mongoid.

Multiple Gemfiles in a rails app

In a large rails app, with several extensions as rails engines, it is a good idea to abstract them into different file. This is to keep the gems required for the app separate from custom built rails engines. We need to create a file in the app folder, and load it inside the main Gemfile.

Gemfile
..........
#default apps
eval(File.read(File.dirname(__FILE__) + '/default_apps.rb'))
#purchased apps
eval(File.read(File.dirname(__FILE__) + '/purchased_apps.rb'))
..........

Passing values to a concern

In order to pass values to a controller concern, we first need to create a variable and bind it to a callback. We will define a variable called app_title and pass it to a method in the concern called fetch_variables. We will use class initialize method to define the variable value.

app/controllers/admin/custom_app_controller.rb
class Admin::CustomAppController < AdminController
  include Authorize
  before_action ->(app_title = @app_title) { fetch_variables app_title }

  def initialize
    super
    @app_title = "custom_app"
  end
end

Now we will define a method in the concern to find and set variable on the callback.

app/controllers/concerns/authorize.rb
module Authorize
  extend ActiveSupport::Concern

  private

  def fetch_variables(app_title)
    @app = App.find_by(key: app_title)
    @app_categories = @app.categories
  end
end

self referential relationships with mongoid

In several work scenarios we need a to create a parent-child relationship on a single model. So, it could be category and subcategory structure in an e-commerce site, or page and sub-page in a CMS. We will use category for our example. We will first generate our category model.

$ rails g model category title:string
      invoke  mongoid
      create    app/models/category.rb
      invoke    rspec
      create      spec/models/category_spec.rb

We need another model where we define the parent and child relationship of category model with itself. We will call this model category_relationship.

$ rails g model category_relationship
      invoke  mongoid
      create    app/models/category_relationship.rb
      invoke    rspec
      create      spec/models/category_relationship_spec.rb

In our category_relationship model, we will now define fields – category_id and child_id. child_id is the reference field for storing the id of the child category. We will define a belongs_to relationship for the parent on the category model and another belongs_to relationship for the child on the same model.

app/models/category_relationship.rb
class CategoryRelationship
  include Mongoid::Document

  field :category_id, type: String
  field :child_id, type: String

  belongs_to :parent, class_name: "Category"
  belongs_to :child, class_name: "Category
end

In our category model, we will define relationships between parent and child. So, has_many and belongs_to relationships, both are made on the same model, and the reference happens via inverse_of.

app/models/category.rb
class Category
  include Mongoid::Document
  field :title, type: String

  has_many :child_category, class_name: 'Category', inverse_of: :parent_category, dependent: :destroy
  belongs_to :parent_category, class_name: 'Category', inverse_of: :child_category
end

Let’s create some category data.

$ rails c
Loading development environment (Rails 4.1.1)
2.1.1 :001 > category1 = Category.new(title: "Apparel")
 => #<Category _id: 53c2279f67617214f0000000, title: "Apparel", parent_category_id: nil>

2.1.1 :002 > category1.save
  MOPED: 127.0.0.1:27017 COMMAND      database=admin command={:ismaster=>1} runtime: 1.9274ms
  MOPED: 127.0.0.1:27017 INSERT       database=learning_development collection=categories documents=[{"_id"=>BSON::ObjectId('53c2279f67617214f0000000'), "title"=>"Apparel"}] flags=[]
                         COMMAND      database=learning_development command={:getlasterror=>1, :w=>1} runtime: 0.9229ms
 => true
2.1.1 :003 > category2 = Category.new(title: "Men")
 => #<Category _id: 53c227d567617214f0010000, title: "Men", parent_category_id: nil>

2.1.1 :004 > category2.save
  MOPED: 127.0.0.1:27017 INSERT       database=learning_development collection=categories documents=[{"_id"=>BSON::ObjectId('53c227d567617214f0010000'), "title"=>"Men"}] flags=[]
                         COMMAND      database=learning_development command={:getlasterror=>1, :w=>1} runtime: 1.0399ms
 => true

We called our relationship parent_category and child_category. So we will use these terms to create and traverse the tree.

2.1.1 :005 > category2.parent_category = category1
 => #<Category _id: 53c2279f67617214f0000000, title: "Apparel", parent_category_id: nil>

2.1.1 :007 > category2.save
  MOPED: 127.0.0.1:27017 UPDATE       database=learning_development collection=categories selector={"_id"=>BSON::ObjectId('53c227d567617214f0010000')} update={"$set"=>{"parent_category_id"=>BSON::ObjectId('53c2279f67617214f0000000')}} flags=[]
                         COMMAND      database=learning_development command={:getlasterror=>1, :w=>1} runtime: 1.2537ms
 => true

Let’s see the result of the relationships we just created. First, we will check the parent.

2.1.1 :008 > category2
 => #<Category _id: 53c227d567617214f0010000, title: "Men", parent_category_id: BSON::ObjectId('53c2279f67617214f0000000')> 

2.1.1 :009 > category2.parent_category
 => #<Category _id: 53c2279f67617214f0000000, title: "Apparel", parent_category_id: nil>

Then, we will check the child. Note that this returns an array, because of the has_many relationship.

2.1.1 :010 > category1.child_category
  MOPED: 127.0.0.1:27017 QUERY        database=learning_development collection=categories selector={"parent_category_id"=>BSON::ObjectId('53c2279f67617214f0000000')} flags=[] limit=0 skip=0 batch_size=nil fields=nil runtime: 1.1054ms

 => [#<Category _id: 53c227d567617214f0010000, title: "Men", parent_category_id: BSON::ObjectId('53c2279f67617214f0000000')>]

Using rails 4 concern to generate slugs

Though concerns have been around since Rails 3.2, they were extracted as a separate feature in Rails 4. At low-level, concerns are basically ruby modules that can be mixed with model or controller classes to make the defined methods available to that class in its context.

Slugs are an important feature in today’s apps where URLs play an important role for several reason. Search Engine Optimization, better user experience, easier bookmarking. There are gems available for creating slugs, like fiendly_id and mongoid-slug. However, a lot of times we need a custom solution. In rails it is easy enough to roll out our own.

We will first create a file for concern inside models.

app/models/concerns/slug.rb
module Slug
  extend ActiveSupport::Concern

  def to_param
    self.title
  end
end

We will call this concern inside our model :

app/models/product.rb
class Product
 include Mongoid::Document
 include Mongoid::Timestamps

 include Slug
 field :name, type: String
end

Now, this uses the default to_param method of Rails. Also, the url generated in this case for a title like, “this is a test”, will look like the following :

http://example.com/this-is-a-test

For SEO reasons, and for the reasons of localization, a lot of times URLs might have a UID.

e.g https://in.news.yahoo.com/arubas-leader-legislators-launch-hunger-strike-180842861.html

In order to generate uid, we will generate a random number and append it to the title. Also we will tie it to the callback in order to generate it before the record is created.

app/models/concerns/slug.rb
module Slug
 extend ActiveSupport::Concern

 included do
   before_create :generate_uid
 end

 def to_param
   [self.title, self.uid].join("-")
 end

private

 def generate_uid
   self.uid = rand(36**8).to_s(36)
 end
end

A couple of things now remain. First, we need a field called title in order to create the slug. What if the model does not have such a field. In mongoid, we can assign a document field a different name. So a field called name, can also be alternatively called as slug.

app/models/product.rb
  field :name,as: slug,type: String

Likewise, we will have to change our slug method to suit this change :

app/models/concerns/slug.rb
module Slug
 extend ActiveSupport::Concern

 included do
   before_create :generate_uid
 end

 def to_param
   [self.title, self.uid].join("-")
 end

private

 def generate_uid
   self.uid = rand(36**8).to_s(36)
 end
end

Lastly, we need to sanitize the string in our title. We will look for white spaces and place dashes instead of them, remove special characters from the string, remove entities.

app/models/concerns/slug.rb
module Slug
 extend ActiveSupport::Concern

 included do
   before_create :generate_uid
 end

 def to_param
   [self.slug.gsub(/[ "'*@#$%^&amp;()+=;:.,?&gt;|\\&lt;~_!]/,'-').gsub(/-{2,}/,'-'), self.uid].join("-")
 end

private

 def generate_uid
   self.uid = rand(36**8).to_s(36)
 end
end

The resultant URL is something like the following:

http://example.com/this-is-a-test-79926128