22 Nov 2014
Two factor authentication upon login:
Pseudo code:
- We need a phone number and pin columns
- 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
20 Nov 2014
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
18 Nov 2014
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:
- Adds your rubygems api so you have to continuouasly log in.
- Lists gems that are remote
15 Nov 2014
Items Covered:
- voteable validations (ajax and regular flows)
- exposing APIs
- extracting common logic from models
- 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
12 Nov 2014
Items covered:
- Slugging
- Single Admin Role
- Timezones
- 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:
- Posts
- Categories
- Users
What are the benefits of Slugging:
- SEO friendly/user ease
- Security (exposing primary key ids)
- 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>