Rapid Prototyping with Rails: Lesson 4, Twilio: Two-factor Auth

Two factor authentication upon login:

Pseudo code:

  1. We need a phone number and pin columns
  2. Change login logic:
  • If phone number not present, normal login
  • If phone number present
    • generate pin to save to db, not show user
    • send pin to Twilio to sms user's phone
    • show a form to enter the pin (generate new route myapp.com/pin)
    • must ensure its secure, can only be seen after user has logged in

1. Add column phone and pin column to users:

- rails g migration add_phone_and_pin_to_users
- class AddPhoneAndPinToUsers < ActiveRecord::Migration
  def change
    add_column :users, :phone, :string
    add_column :users, :pin, :string
  end
end

2. Add phone input field to your users/new.html.erb

<div class='control-group'>
  <%= f.label :phone, "Phone Number <small>(numbers only)</small>".html_safe %>
  <%= f.text_field :phone, maxlength: 9 %>
  <%= link_to '#', id: 'two_factor_phone' do %>
    <i class='icon-question-sign'></i>
  <% end %>
</div>

3. Also, we need to add :phone to strong_parameters

def user_params
  params.require(:user).permit(:username, :password, :time_zone, :phone)
end

4. So after the phone number is entered by the user, we want to prevent them from

logging in directly and prompt them for their pin and connect with Twilio. The login occurs in the sessions controller:

def create
  user = User.where(username: params[:username]).first
  if user && user.authenticate(params[:password])
    ****if user.two_factor_auth? #must create this method

    else
      session[:user_id] = user.id
      flash[:notice] = "Welcome, you've logged in."
      redirect_to root_path
    end
  else
    flash[:error] = "There is something with your username & password."
    redirect_to login_path
  end
end

5. Create twofactorauth? method in your user model:

def two_factor_auth?
    !self.phone.blank?
  end

6. We have to now breakdown what else we need

def create
  user = User.where(username: params[:username]).first
  if user && user.authenticate(params[:password])
    if user.two_factor_auth?
      #generate a pin
      user.generate_pin!
      #send pin to twilio, sms user's phone
      #show pin form for user input after sms
    else
      session[:user_id] = user.id
      flash[:notice] = "Welcome, you've logged in."
      redirect_to root_path
    end
  else
    flash[:error] = "There is something with your username & password."
    redirect_to login_path
  end
end

7. Create generate_pin! method:

def generate_pin!
    self.update_column(:pin, rand(10 **6)) #generate a random six digit number
  end

8. Skip the send pin to twilio part for now

9. Show pin form for the user input:

#routes.rb:
get '/pin', to: 'sessions#pin'
post '/pin', to: 'sessions#pin'

10. Adding the above routes, gives us the pin_path route which we can redirect to:

def create
  user = User.where(username: params[:username]).first
  if user && user.authenticate(params[:password])
    if user.two_factor_auth?
      #generate a pin
      user.generate_pin!
      #send pin to twilio, sms user's phone

      #show pin form for user input after sms
      redirect_to pin_path

    else
      session[:user_id] = user.id
      flash[:notice] = "Welcome, you've logged in."
      redirect_to root_path
    end
  else
    flash[:error] = "There is something with your username & password."
    redirect_to login_path
  end
end

11. Now create a pin.html.erb file under views

12. Now when attempting to go to the get '/pin' upon being redirected to the pin_path,

notice that the get method work without even creating the action. ** When there is a link, the default is to render. This will not work when the method post is called bc you are inputing parameters:

#sessions_controller.rb
def pin
  if request.post?
    user = User.find_by(pin: params[:pin])
    if user
      #remove pin
      user.remove_pin!
      #normal login success route
      login_user!(user)
    else
      flash[:error] = "Sorry, something is wrong with the pin you've entered."
      redirect_to pin_path
    end
  end
end

private

def login_user!(user)
  session[:user_id] = user.id
  flash[:notice] = "Welcome, you've logged in."
  redirect_to root_path
end

13. Now we must set prevent users from going to the '/pin' url by entering it

in their browser: We can do so by setting a session hash to true/false after login attempt/sucessful login in the sessions_controller:

#sessions_controller.rb

#login attempt
  def create
    user = User.where(username: params[:username]).first
    if user && user.authenticate(params[:password])
      if user.two_factor_auth?
        session[:two_factor] = true
#successful login
def pin
  if request.post?
    user = User.find_by(pin: params[:pin])
    if user
      session[:two_factor] = nil
      #remove pin
      .
      .
      .

14. Now we can lock down the url using our access_denied method we previously created:

We can put this in the session#pin bc it impacts both the get and post

#sessions_controller.rb

def pin
  access_denied if session[:two_factor].nil?
  if request.post?
    user = User.find_by(pin: params[:pin])
    if user
      session[:two_factor] = nil
      #remove pin
      user.remove_pin!
      #normal login success route
      login_user!(user)
    else
      flash[:error] = "Sorry, something is wrong with the pin you've entered."
      redirect_to pin_path
    end

15. remove.pin! method

#user.rb
def remove_pin!
  self.update_column(:pin, nil)
end

16. Now its time to intergrate Twilio

- install the Twilio-Ruby gem

17. Create a method to send to text to user using Twilio api

#sessions_controller.rb
def create
  user = User.where(username: params[:username]).first
  if user && user.authenticate(params[:password])
    if user.two_factor_auth?
      session[:two_factor] = true
      #generate a pin
      user.generate_pin!
      #send pin to twilio, sms user's phone
      user.send_pin_to_twilio
      #show pin form for user input after sms
      redirect_to pin_path

18. Create sendpinto_twilio method in user.rb (using Twilio two factor authentication)

def send_pin_to_twilio
  account_sid ='AC47bdb7f569ae31aa59309f81981aaf37'
  auth_token = 'f3d9c378bba9272d3ed9fec659e353b0'

  # set up a client to talk to the Twilio REST API
  client = Twilio::REST::Client.new account_sid, auth_token
  msg = "Hello, Please enter this pin to continue login: #{self.pin}."
  account = client.account
  message = account.sms.messages.create({
    :from => '123-456-789',
    :to => '123-456-789',
    :body => msg,
    })
  end

Rapid Prototyping with Rails: Lesson 4, Comparing Metaprogramming vs. Concerns

There are two options for Extracting Common code:

  • Metaprogramming
  • Modules/Concerns

Closer look at Metaprogramming

Metaprogamming will allow you to include a module that will allow your Instance/Class methods to run as class/instance methods:

module Votetable
  def self.included(base)
    base.send(:include, InstanceMethods)
    base.extend ClassMethods
  end

So for class methods:

module Votetable
  def self.included(base)
    base.send(:include, InstanceMethods)
    base.extend ClassMethods
    base.class_eval do
      my_class_eval
    end
  end

  class InstanceMethods
    def total_votes
      self.up_votes - self.down_votes
    end

    def up_votes
      self.votes.where(vote: true).size
    end

    def down_votes
      self.votes.where(vote: false).size
    end
  end

  class ClassMethods
    def my_class_method
      puts "This is class method"
    end
  end
end

One thing to understand is that you can organize the code below into a sub-module to orchestrate how the code is called:

module Votetable
  def self.included(base)
    base.send(:include, InstanceMethods)
    base.extend ClassMethods
    base.class_eval do #calling the class method each time the Post class is called
      my_class_eval
    end
  end

So if you went into rails console and called Post => "This is class method" would load, in addition to the Post object.

Closer look at ActiveSupport::Concerns (Modules)

We can also extract our has_many :votes, as: :voteble association, but we can place this in a class method, which will allow it to be fired automatically when the Post class is called just as though its in the model:

class ClassMethods
     has_many :votes, as: :voteable
   end

Using Concerns

module Voteable
     extend ActiveSupport::Concern

     included do
       has_many :votes, as: :voteable
     end


     def total_votes
       self.up_votes - self.down_votes
     end

     def up_votes
       self.votes.where(vote: true).size
     end

     def down_votes
       self.votes.where(vote: false).size
     end
   end

Using Metaprogramming

module Voteable
     def self.included(base)
       base.send(:include, InstanceMethods)
       base.extend ClassMethods
     end

     class InstanceMethods

       def total_votes
         self.up_votes - self.down_votes
       end

       def up_votes
         self.votes.where(vote: true).size
       end

       def down_votes
         self.votes.where(vote: false).size
       end
     end

     class ClassMethods
       has_many :votes, as: :voteable
     end
   end

Rapid Prototyping with Rails: Lesson 4, ActiveSupport::Concerns

DRYing up code with ActiveSupport::Concerns

1. Extract common code from Post/Comment Model abd place into Sluggable module
module Slugglable
  extend ActiveSupport::Concern

  included do
    before_save :generate_slug!
  end

  def to_param
    self.slug
  end

  def generate_slug!
    the_slug = to_slug(self.title)
    post = Post.find_by slug: the_slug
    count = 2
    while post && post != self
      the_slug = append_suffix(the_slug, count)
      post = Post.find_by slug: the_slug
      count += 1
    end
    self.slug = the_slug.downcase
  end

  def append_suffix(str, count)
    if str.split('-').last.to_i != 0
      return str.split('-').slice(0...-1).join('-') + "-" + count.to_s
    else
      return str + "-" + count.to_s
    end
  end

  def to_slug(title)
    str = title.strip
    str.gsub! /\s*[^A-Za-z0-9]\s*/, '-'
    str.gsub! /-+/, "-"
    str.downcase
  end
end


2. One major problem with this is that the module has to work for both comments and posts
yet the model and column names are currently hardcoded.
The first fix is changing the class to thed class to self.class:

    def generate_slug!
    the_slug = to_slug(self.title)
    post = self.class.find_by slug: the_slug
    count = 2
    while post && post != self
      the_slug = append_suffix(the_slug, count)
      post = self.class.find_by slug: the_slug
      count += 1
    end

3. Now to change the column names, you have to create a class method that we can call to
 pass the column name:

 module Slugglable
   extend ActiveSupport::Concern

   included do
     before_save :generate_slug!
     class_attribute: :slug_column
   end

   def to_param
     self.slug
   end

   def generate_slug!
     the_slug = to_slug(self.send(self.class.slug_column.to_sym))
     post = self.class.find_by slug: the_slug
     count = 2
     while post && post != self
       the_slug = append_suffix(the_slug, count)
       post = self.class.find_by slug: the_slug
       count += 1
     end
     self.slug = the_slug.downcase
   end

   def append_suffix(str, count)
     if str.split('-').last.to_i != 0
       return str.split('-').slice(0...-1).join('-') + "-" + count.to_s
     else
       return str + "-" + count.to_s
     end
   end

   def to_slug(title)
     str = title.strip
     str.gsub! /\s*[^A-Za-z0-9]\s*/, '-'
     str.gsub! /-+/, "-"
     str.downcase
   end


   module ClassMethods
     def sluggable_column(col_name)
       self.slug_column = col_name
     end
   end

 end

 5. Replace the local variable, post
 def generate_slug!
   the_slug = to_slug(self.send(self.class.slug_column.to_sym))
   obj = self.class.find_by slug: the_slug
   count = 2
   while obj && obj != self
     the_slug = append_suffix(the_slug, count)
     obj = self.class.find_by slug: the_slug
     count += 1
   end
   self.slug = the_slug.downcase
 end
 6. Define sluggable_column in Post, User, and Category Models

    sluggable_column :title

Rails Console Commands:

cat ~/.gem/credentials
  • Adds your rubygems api so you have to continuouasly log in.
gem list -r voteable_jam
  • Lists gems that are remote

Rapid Prototyping with Rails: Lesson 4, part 3

Items Covered:

  1. voteable validations (ajax and regular flows)
  2. exposing APIs
  3. extracting common logic from models
  4. creating/publishing gem

Voteable Validation:

We want to prevent users from voting on the same post or comment twice, but at the same time, allow them to vote on multiple posts, comments, etc.

class Post < ActiveRecord::Base
  validates_uniqueness_of :creator, scope: :voteable
end

Showing Js Errors when voting

You have two options: 1. Alterting out (currently implemented) 2. Traverse the DOM

Just like with using to change the number of votes, we can also create a div.

#posts/_post.html.erb
  <div id="post_vote_error_<%= post.to_param %>" class="alert alert-error" style="display: none">
    You can only vote once.
  </div>
#posts/vote.js.erb
  if @vote.valid? %>
    ('#post_<%= @post.to_param %>_votes').html('<%= @post.total_votes %>');
  else %>
    ('#post_vote_error_<%= @post.to_param %>').show().html('<%= @vote.errors.full_messages.join'('') %>);
  end

APIs

APIs are how applications talk to each other. API versioning is a big deal - you have to have a versioning strategy, typically done by the date.


Extracting common logic from models

When you have common code in your models, you really have tow options to DRY your code:
1. place code in your superclass.
2. Extract code into a module

Extracting Common Logic from Models
-One way to do so is through module which is the next is Post.ancestors lookup chain.
1. Go into your application.rb file

  config.autoload_paths += %W(#{config.root}/lib)

So what this line of code does, is it directs your rails application to load t
his path and files in it everytime your application starts up, including the
voteable.rb file found

2. So just like with modules how we add '-able' on the end of our module name,
 we create a file called Voteable.rb under the lib folder:
 #lib/voteable.rb

  module Voteable

    class InstanceMethods

    end


    class ClassMethods

    end
  end

3. ActiveSupport::Concern
ActiveSupport is a gem.
ActiveSupport::Concern (comes with Rails 4) says that all the instance methods
listed within it are going to be instance methods when you mixin the module.

4. To include class methods within the module:

module Votable
  extend ActiveSupport::Concern

  def total_votes


  end


  module ClassMethods
    def my_class_method


    end
  end
end

5. Add the common methods the are to be extracted from your model

  module Votable
    extend ActiveSupport::Concern

      def total_votes
        self.up_votes - self.down_votes
      end

      def up_votes
        self.votes.where(vote: true).size
      end

      def down_votes
        self.votes.where(vote: false).size
      end

    end
  end

6. Add include Voteable to your model

7. Now if you run Post.ancestors => the Voteable module will be right behind the
  Post class.

8. Modules also give us an included do block:

  included do
    puts "I'm being included"
  end

  The first time the object is called, "I'm being called is printed"

9. This is a perfect candidate for has_many :votes, as: :voteable

  included do
    has_many :votes, as: :voteable
  end

*can be done for the comment and posts

Creating a Gem!

So we were able to extract common methods into a module, but what if we needed the same
functionality across several projects:

- to list gems in terminal: gem list gem

1. You need gem: gemcutter (0.7.1)
2. mkdir voteable-gem
3. Create voteable.gemspec file within folder
4. Add the following tex t:

  Gem::Specification.new do |s|
    s.name = "votable_chris_oct"
    s.version = '0.0.0'
    s.date = '2013-10-23'
    s.summary = "A voting gem"
    s.description = "The best voting gem ever"
    s.authors = ['Jamela B.']
    s.email = 'jamela.black@gmail.com'
    s.files = ['lib/voteable_chris_oct.rb']
    s.homepage = 'http://github.com'
  end

5. Create lib folder, and voteable_chris_oct.rb file within it
6. Cut and paste common code from module

  Module VoteableChrisOct
    extend ActiveSupport::Concern

      def total_votes
        self.up_votes - self.down_votes
      end

      def up_votes
        self.votes.where(vote: true).size
      end

      def down_votes
        self.votes.where(vote: false).size
      end

    end
  end

7. Now, using gemcutter, type in terminal:

    gem build voteable.gemspec

8. Then, push to rubygems.org:

    gem push voteable_chris_oct-0.0.0.gem

9. To list your newly made gem to make sure
the push was successful:

  gem list -r voteable_chris_oct

10.  THEN ADD GEM TO YOUR GEM FILE

    gem 'voteable_chris_oct
____________________________________
You don't have to do this apparently.

10. Next you must include the file name in your application.rb file

  require 'voteable_chris_oct'

11. The include the module name in your model
   include VoteableChrisOct
_______________________________________

12. For changes or updates to your gem:
You want to change your version number in your gemspec, run gem build and push to
rubygems. Then you may want to specify the gem version in your gemfile

  gem 'voteable_chris_oct', '=0.1.0'

13. What if you don't want to publish everytime you make a change and
you just want test locally, then in your gem file you only have to specify the path up to the parent directory:

  gem 'voteable_chris_oct', path: (just run pwd in your terminal)

14. To remove a gem from rubygems, just run the following in your terminal:

  gem yank voteable_chris_oct -v '0.0.0'

15. When making changes to your gem file, you should run
bundle install --without production

16. Changes made within your module are hotlinked to your app so all changes are made live without updating your gem version

Rapid Prototyping with Rails: Lesson 4, part 2

Items covered:

  1. Slugging
  2. Single Admin Role
  3. Timezones
  4. Users select their own timezone

Slugging

a custom URL generated based off of some characteristic of the page being viewed.

In our case, we care about from a slugging perspective:

  1. Posts
  2. Categories
  3. Users

What are the benefits of Slugging:

  1. SEO friendly/user ease
  2. Security (exposing primary key ids)
  3. Prevent those from knowing how many users you have

To set up a slug:

#posts/_post.html.erb

<span>
  <%= link_to('#{post.comments.size} comments', post_path(post))
</span>

* the post_path(post) is actually calling the to_params method on the post object:

<span>
<%= link_to ('#{post.comments.size} comments', post_path(post.to_params)) %>
</span>

1. So how can we change the to_params method?

Within our model, we can declare an instance method where the to_params method will go to our slug instead of the params id.

def to_param
  self.slug
end

2. We must also create a migration to add our slug column to our existing table:

rails g migration add_slug_to_posts

3. Open Migration File

change :posts |t| do
    add_column :posts, :slug, :string
  end

4. In Model, we must create a method to generate the slug.

Take time to create a method using gsub/regex (for edge cases)

test out regex on rubylur.com

One example:

def generate_slug
  self.slug = self.title.gsub(' ', '-').downcase
  self.save # we hate calling .save explicitly, maybe we can add it a callback.
end

5. ActiveRecord Callbacks (look up online, listed in order of workflow)

Methods the are exposed to use as a apart of the lifecycle of an ActiveRecord object, so we can insert or make changes to any of the callbacks.

A couple things to consider - do we want to generate the slug after created only, or after the post is created and updated?

We call insert add generate_slug method after @post.save:

after_save :generate_slug

But we dont want to create slugs off of bad urls!

So how about:
    after_validations :generate_slug
    or
    before_save :generate_slug
  • This code goes in the model
  • Know the difference between
before_save  - update slug whenever title changes
before_create

6. So if I go with before_save and then all my existing posts will blow up.

Why? because they do not have slugs. So I must go into to the Rails console and run Posts.all.each {|post| post.save} This is will trigger the before_save action and create slugs for all existing posts before saving.

The above console command is not good for when in production, better to run a migration.

7. Now when visiting the link:

<span>
  link_to('#{post.comments.size} comments', post_path(post))
</span>

When using named routes, always use objects instead of hard-coding, bc on objects you can call to_params and its useful in case you want to sluggify.

Now we must update the setpost method in our postscontroller since we've changed the to_param method:

OLD:
  def set_post
    @post = Post.find(params[:id])
  end
NEW:

def set_post
  @post = Post.find_by(slug: params[:id])
end

8. Same thing goes for our comment which uses @post

#comments_controller

  def create
    @post = Post.find_by(slug: params[:post_id])
  end
  • One of the best slugging gems is friendly_id

9. Update the span id

<span id='post_<%=post.id%>_votes>' to be

<span id='post_<%=post.slug'%>_votes'> in your _post.html.erb partial and your
  vote.js.erb

Single Admin Role

Define a number of roles and each roles has a set of permissions User has many roles, A Role has many permissions.

You must create has_many through association. Over all, having various roles with permissions is discouraged because every action is required to be checked against a set of permissions and this will not only greatly complicate development and slow the application down.

For simple apps, a single admin role is typically best This requires having a role column on users that takes a string as free form text, which allows you to specify whatever role you want. This doesnt allow for the most flexiiblity but will give you keep you from having to create a roles table and permissions table.

1. Create migration to add roles to users table:

rails g migration add_roles_to_users

2. Open migration file, add syntax

def change
    add_column :users, :role, :string

  end

3. Create roles in user model

def admin?
    self.admin == 'admin'
  end

  def moderator?
    self.role == 'moderator?'
  end
  • Or you can use a symbol for performance optimization
def admin?
    self.role.to_sym == :admin
  end

  def moderator?
    self.role.to_sym == :moderator
  end

4. So what if we said "In order to create a category, you must be an admin"

We could create a beforeaction :requireadmin in the categories_controller

class CategoriesController < ApplicationController
    before_action :require_user
    before_action :require_admin
  end

5. We want to create the requireadmin method in the applicationcontroller as

we did with requireuser. We want to make sure that the user is loggedin? . If the currentuser is an an admin but not loggedin, an exception will be thrown because you must consider nil condition.

def require_admin
  access_denied unless logged_in? and current_user.admin?
end

def access_denied
  flash[:error] = "Sawry.. you can't do that."
  redirect_to root_path
end

6. Move New Category Link (and New Post Link) under if loggedin? under _navigation.htmlerb

#layout/_navigation.html.erb

<% if logged_in? and current_user.admin?
  <li>
    <%= link_to "New Post", new_post_path %>
  </li>
  <li>
    <%= link_to "New Category", new_category_path %>
  </li>
  <% end %>
<% end %>

Timezones

1. We have an existing helpermethod in our applicationhelper.rb that displays timezone,

we can just add %Z to include timezone:

def display_datetime(dt)
    dt.strftime("%m%d%Y %1:%M%P %Z")
  end

2. In your application.rb file, uncomment out:

config-time_sone = 'Central Time (US & Canada)'

3. Now we need to find the string for setting the default time to Eastern Standard Time

- run rake -T | grep time => rake time:zones:all
  - run rake time:zones:all => displays all timezones available for rails
  - run rake time:zones:all | grep US => 'Eastern Time (US & Canada)'

4. Anytime you make changes to the application.rb file, you must restart the server


Users select their own timezone

This is great, but what if we want users to select their own timezone:

1. Create Rails migration add timezone column to users

rails g migration addtimezonesto_users

2. class AddTimeZonesToUsers < ActiveRecord::Migration

def change
      add_column :users, :time_zones, :string
    end
  end

3. We want to add the ability for users to select their timezone upon registration:

<div class='control-group'>
  <%= f.label :time_zone %>
  <%= f.time_zone_select :time_zone, ActiveSupport::TimeZone.us_zones %>
</div>

This timezoneselect comes with Rails 4 only and newer. The ActiveSupport::TimeZone.us.zones will display all US timezones at the top. Reference documentation for more option

4. Check what data can be submitted by adding a binding.pry to the user#create

action and running params in rails console

5. userparams will show what params have been submitted, and we can see that timezone was not saved because we need to add it to strongparams

def user_params
      params.require(:user).permit(:username, :password, :time_zone)
    end

6. Once logged in, we want the users timezone to be displayed, we must edit the display_datetime method

in the ApplicationHelper:

  module ApplicationHelper
    def display_datetime(dt)
      if logged_in? && !current_user.time_zone.blank?
        dt = dt.in_time_zone(current_user.time_zone)
      end
    end
  end

Time_zone method will display the object's time if its passed a string. So in rails console,if you run post.created_at.in_time_zone("Arizona") => will return the datetime object

To understand more about the timezoneselect or the intimezone method, look up the documentation.

7. If we wanted to specify the default time in our drop down

<div class='control-group'>
    <%= f.label :time_zone %>
    <%= f.time_zone_select :time_zone, ActiveSupport::TimeZone.us_zones, default: Time.zone.name%>
  </div>