When creating any rails project, restful_authentication is usually one of the first things I install. Although it is a great plugin, adding all the extra login stuff can be quite a task. I found this railsforum thread very helpful, but not exactly what I wanted. What follows is what I’ve come up with so far. Some bits of code from the railsforum thread do not readily appear to be used, but have been left in to be used at a later point.

The first thing I did after creating my project was too install these two plugins which have information available at these links.

Install the plugins and the generate the base code we’ll work with from these commands. The articles scaffold is not entirely needed and you could choose to create a different model entirely. It is used here to help illustrate. Each line starting with ./script is one line. Be careful if you are copying and pasting.


./script/plugin install http://svn.techno-weenie.net/projects/plugins/restful_authentication
./script/plugin install http://elitists.textdriven.com/svn/plugins/acts_as_state_machine/trunk
./script/generate authenticated user sessions --include-activation --stateful
./script/generate scaffold article title:string body:text
./script/generate controller passwords new create edit update
./script/generate controller settings password

Before we forget, delete the index.html file in the public directory as well as the app/views/layouts/articles.html.erb layout file which was generated by the scaffolding. Our users migration is a little different. It has been changed here to reflect how it is done with rails 2.0, but that is not necessary. Note the role column and absence of a password_reset_code.

class CreateUsers < ActiveRecord::Migration
  def self.up
    create_table "users", :force => true do |t|
      t.string :login, :email, :null => :no
      t.string :crypted_password, :salt, :limit => 40
      t.string :remember_token
      t.datetime :remember_token_expires_at, :activated_at, :deleted_at
      t.string :activation_code, :limit => 40
      t.string :state, :null => :no, :default => 'passive'
      t.string :role, :null => :no, :default => 'subscriber'
      t.boolean :enabled, :default => false
      t.timestamps
    end
  end

  def self.down
    drop_table "users"
  end
end

With rails 2.0, sqlite3 is the default db. You do not need to configure anything else to get your app up and running. I will skip configuring a mysql db because of this. If mysql is your thing, go ahead and get that set up now. Run the migration with this command

rake db:migrate

Next, instead of creating a separate config/environment/mail.rb file I just added to config/environment.rb. Don’t forget about the user_observer line.

config.active_record.observers = :user_observer
config.action_mailer.smtp_settings = {
  :address => "mail.yourapp.com",
  :port => 25,
  :domain => "yourapp.com",
  :authentication => :login,
  :user_name => "username",
  :password => "password"
  }

Now we need to add a couple of methods to our app/models/user_mailer.rb file. Make sure you do not add these as protected methods. At this point in time, I’m not requiring users to to verify their email address, because of this, a small change is needed to the signup_notification method. Add the appropriate info to the setup_email method as well.

# change the @body[:url] line in this method
def signup_notification(user)
  setup_email(user)
  @subject    += 'Please activate your new account'
  @body[:url]  = "http://yoursite.com/"
end

# add the two methods below
def forgot_password(user)
  setup_email(user)
  @subject    += 'You have requested to change your password'
  @body[:url]  = "http://yoursite.com/reset_password/#{user.activation_code}"
end

def reset_password(user)
  setup_email(user)
  @subject    += 'Your password has been reset.'
end

We can skip the generation of roles and permissions and move straight onto editing our user model. Below are the additions I made to the user model.

# Add this as the first line underneath the class declaration
ROLES = ['subscriber', 'admin']
...
# Add this to the block of validation methods
validates_inclusion_of    :role, :in => ROLES, :on => :create
...
# These are the public methods I added
def check_auth(pw)
  if User.authenticate(self.login, pw)
    @change_pw = true
  else
    errors.add(:old_password, "does not equal current password")
    @change_pw = false
  end
end

def forgot_password
  @forgot_pw = true
  self.make_activation_code
  self.save
end

def reset_password(pw, confirm)
  @reset_pw = true
  update_attribute(:activation_code, nil)
  update_attributes(:password => pw, :password_confirmation => confirm)
end

def forgot_pw?
  @forgot_pw
end

def reset_pw?
  @reset_pw
end

def change_pw?
  @change_pw
end

def self.find_for_forget(email)
  find :first, :conditions => ['email = ? and activation_code is null', email]
end

def has_role?(name)
  self.role.eql?(name) ? true : false
end

# Modify this private method
def password_required?
  crypted_password.blank? || !password.blank? || @change_pw || @reset_pw
end

I had not thought about using instance variables to hold boolean values in the way that they are used here. While doing this I ran into a few mysterious problems that were traced right back to them. What caused the problems was that I was expecting them to be set but they weren’t being set till after they were being used. Consequently, things just weren’t working. Bottom line, be careful where you place those boolean variables when using them in this way. Another thing to note, the validates_inclusion_of_role only happens on create right now. This is not an ideal solution because users roles should only be what is included in the list in the model and this could allow the role to be changed to anything after they have been created.

I know that adding this code to lib/authenticated_system.rb is not a best practice. For now though, it will do it as a temporary solution.

def not_logged_in_required
  !logged_in? || permission_denied
end

def check_role(role)
  if logged_in?
    @current_user.has_role?(role) ? true : permission_denied
  else
    access_denied
  end
end

def admin_required
  check_role('administrator')
end

# this is not the full method like the one provided at the railsform
# thread. it gets the job done now so we can flesh out the rest
def permission_denied
  respond_to do |format|
    format.html do
      flash[:error] = "You don't have permission to complete that action."
      redirect_to home_path
    end
  end
end

My users and sessions controllers stayed mostly the same. Don’t forget to move the ‘include authenticatedSystem’ line into the application controller. Filters have been added to both and I changed the create function of the users_controller to reflect not needing to verify email address’ after signup. Users pass up pending and go straight to active.

# users_controller.rb
# these are the filters
before_filter :admini_required, :only => [:destroy, :suspend, :unsuspend, :purge]
before_filter :not_logged_in_required, :only => [:new, :create]
before_filter :find_user, :only => [:suspend, :unsuspend, :destroy, :purge]

# this is my modified create action. Still pretty much the same
def create
  cookies.delete :auth_token
  @user = User.new(params[:user])
  if @user.save
    self.current_user = @user
    current_user.activate!
    redirect_back_or_default(home_path)
    flash[:notice] = "Thanks for signing up!"
  else
    render :action => 'new'
  end
end

# sessions_controller.rb
# these are the filters I used, nothing else changed here
before_filter :login_required, :only => :destroy
before_filter :not_logged_in_required, :only => [:new, :create]

Part of the create method provided by restful_authentication looks like this

@user = User.new(params[:user])
@user.register? if @user.valid?
if @users.errors.empty?
  finish rest of the method...

I am not sure why they are checked that way. Hopefully it is not for plugging up a security hole. If anyone knows why it is done this way, please leave a comment.

If you checked out the bells and whistles walk through, you will notice a few different choices in implementation here. After messing around with the code a bit, I decided to use the passwords scaffold like the tutorial, but instead of the accounts controller, I created a settings controller. Very similar yes, but still different. The settings controller deviates a bit from the restful philosophy and could potentially cause some to complain, but it appears to work well at this point.

The passwords controller is used only if you have forgotten your password and need to reset it. This separates the logic required to determine if a password reset is needed as opposed to users who just want to change their passwords voluntarily.

The settings controller will not only control changing passwords, but also other personal information related to each user such as their email address, profile information and even their profile image. Of course, the only part we are worried about right now is the password part. If you’re a member of digg, you might notice this app is being set up in a similar manner to the way their urls are laid out. Users can change some of their settings with urls in the form of settings/password or settings/email. For a few of the important settings, this doesn’t seem too bad. I can see it getting ugly if not careful, but a few key settings should be fine.

If you used the generate command from above for your passwords controller, you can delete the create and edit views too. The code for the passwords_controller is

class PasswordsController < ApplicationController
  before_filter :not_logged_in_required

  #/forgot_password
  def new
  end

  #/mail_reset_code
  def create
    return unless request.post?
    @user = User.find_by_email(params[:email])
    if @user
      @user.forgot_password
      flash[:notice] = "A password reset link has been sent to #{@user.email}."
      redirect_to home_path
    else
      flash[:error] = "Could not find a user with that email address."
      redirect_to forgot_password_path
    end
  end

  #/reset_password/:password_reset_code
  def edit
    @code = params[:activation_code]
    @user = User.find_by_activation_code(@code)
    if request.post?
      @password, @confirmation = params[:password], params[:password_confirmation]
      if @user.reset_password(@password, @confirmation)
        flash[:notice] = "Password reset. You can now login"
        redirect_to login_path
      end
    else
      unless @user
        flash[:error] = "Sorry, that is an invalid password reset code"
        redirect_to forgot_password_path
      end
    end
  end
end

and for now the code for the settings_controller is

class SettingsController < ApplicationController
  before_filter :login_required, :find_user

  def password
    if request.put?
      @pw, @confirm = params[:password], params[:password_confirmation]
      if @user.check_auth(params[:old_password]) &&
                          @user.update_attributes(:password => @pw, :password_confirmation => @confirm)
        flash[:notice] = "Your password has been changed"
        redirect_to password_path && return
      end
    end
  end

  protected

  def find_user
    @user = User.find(current_user.id)
  end
end

Now it is time to create all of our views.

My layout/application.rb looks like this

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
       "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
  <meta http-equiv="content-type" content="text/html;charset=UTF-8" />
  <title>Fix the title</title>
  <%= stylesheet_link_tag 'base' %>
  <%= javascript_include_tag :defaults %>
</head>
<body>
  <div id="wrapper">
    <div id="header">
      <h1><%= link_to "This is a site", home_path %></h1>
      <% if logged_in? %>
        <p>Hello <%= current_user.login %> | <%= link_to 'Sign Out', logout_path %> | <%= link_to 'Change Password', password_path %></p>
    <% else %>
        <p><%= link_to 'Sign Up', signup_path %> or <%= link_to 'Login', login_path %></p>
      <% end %>
    </div>
    <div id="content">
      <% flash.each do |key, msg| %>
        <%= content_tag :div, msg, :id => key %>
      <% end %>
      <%= yield %>
    </div>
    <div id="footer">
      <p>copyright 2008 Dan Engle</p>
    </div>
  </div>
</body>
</html>

The views are as follows

# app/views/passwords/edit.html.erb
<h3>Choose a new password</h3>
<%= error_messages_for :user %>
<% form_tag url_for(reset_password_path) do %>
  <%= hidden_field_tag :activation_code, @user.activation_code %>
  <p>
    <label for="password">Password:</label>
    <%= password_field_tag :password %>
  </p>
  <p>
    <label for="confirm_password">Confirm Password:</label>
    <%= password_field_tag :password_confirmation %>
  </p>
  </p>
    <%= submit_tag "Reset Your Password" %>
  </p>
<% end %>

# app/views/passwords/new.html.erb
<h3>Forgot Password</h3>
<% form_tag url_for(mail_reset_code_path) do %>
  <p>
    <label for="email">What is the email address used to create your account?</label>
    <%= text_field_tag :email %>
  </p>
  <p>
    <%= submit_tag 'Reset Password' %>
  </p>
<% end %>

# app/views/settings/password.html.erb
<h3>Change your password</h3>
<%= error_messages_for :user %>
<% form_for(@user, :url => password_path) do %>
  <p>
    <label for="old_password">Old Password</label>
    <%= password_field_tag :old_password %>
  </p>
  <p>
    <label for="new_password">New Password</label>
    <%= password_field_tag :password %>
  </p>
  <p>
    <label for="old_password">Confirm Password</label>
    <%= password_field_tag :password_confirmation %>
  </p>
  <p>
    <%= submit_tag "Change Password" %>
  </p>
<% end %>

# app/views/sessions/new.html.erb
<h3>Login</h3>
<% form_tag session_path do -%>
  <p>
    <label for="login">Login</label>
    <%= text_field_tag 'login' %>
  </p>
  <p>
    <label for="password">Password</label>
    <%= password_field_tag 'password' %>
  </p>
  <p>
    <label for="remember_me">Remember me:</label>
    <%= check_box_tag 'remember_me' %>
  </p>
  <p>
    <%= submit_tag 'Log in' %>
  </p>
<% end -%>
<p><%= link_to 'Forgot password?', forgot_password_path %></p>

# app/views/user_mailer/forgot_password.html.erb
<%=h @user.login %>, to reset your password, please visit
<%= @url %>

# app/views/user_mailer/reset_password.html.erb
<%= @user.login %>, Your password has been reset

The only views left are those of the articles model. I am including them as a quick reference for those who created the articles scaffold and just want this to work.

# app/views/articles/_article.html.erb
<% div_for article do -%>
  <h3><%= article.title %></h3>
  <p><%= article.body %></p>
<% end -%>

# app/views/articles/_form.html.erb
<p>
  <label for="title">Title:</label>
  <%= f.text_field :title %>
</p>
<p>
  <label for="body">Body:</label>
  <%= f.text_area :body %>
</p>

# app/views/articles/edit.html.erb
<h1>Editing article</h1>
<%= error_messages_for :article %>
<% form_for(@article) do |f| %>
  <%= render :partial => 'form', :locals => {:f => f} %>
  <p>
    <%= f.submit "Update" %>
  </p>
<% end %>
<%= link_to 'Show', @article %> |
<%= link_to 'Back', articles_path %>

# app/views/articles/index.html.erb
<h1>Listing articles</h1>
<%= render :partial => @articles %>
<% if admin? %>
  <%= link_to 'New article', new_article_path %>
<% end %>

# app/views/articles/new.html.erb
<h1>New article</h1>
<%= error_messages_for :article %>
<% form_for(@article) do |f| %>
  <%= render :partial => 'form', :locals => {:f => f} %>
  <p>
    <%= f.submit "Create" %>
  </p>
<% end %>
<%= link_to 'Back', articles_path %>

# app/views/articles/show.html.erb
<% if admin? %>
  <p><%= link_to 'Edit', edit_article_path(@article) %></p>
<% end %>
<h1><%= @article.title %></h1>
<p><%= @article.body %></p>
<p><%= link_to 'Back', articles_path %></p>

Some of the views in article only display links if you are an administrator. In order to make that happen, you need to add this helper to app/helpers/application_helper.rb

def admin?
  return true if logged_in? && current_user.has_role?('administrator')
end

To finish up, config/routes.rb file should look like this

ActionController::Routing::Routes.draw do |map|
  map.home '/', :controller => 'articles', :action => 'index'
  map.resources :articles

  map.resources :users, :member => {:suspend => :put,
                                    :unsuspend => :put,
                                    :purge => :delete}

  map.activate '/activate/:activation_code', :controller => 'users', :action => 'activate'
  map.forgot_password '/forgot_password', :controller => 'passwords', :action => 'new'
  map.mail_reset_code '/mail_reset_code', :controller => 'passwords', :action => 'create'
  map.reset_password '/reset_password/:activation_code', :controller => 'passwords', :action => 'edit'

  map.password '/settings/password', :controller => 'settings', :action => 'password'

  map.resource :session
  map.signup '/signup', :controller => 'users', :action => 'new'
  map.login  '/login', :controller => 'sessions', :action => 'new'
  map.logout '/logout', :controller => 'sessions', :action => 'destroy'
end

That about completes this portion of enhancing restful_authentication. Users can register themselves, reset their passwords if they forget them, and change them voluntarily. It is far from complete, but I didn’t realize how much writing and posting of code I would actually be doing. Either way, I hope you found this portion useful. Let me know if any corrections are needed or if you have any ideas on improvements.

If you find any amateur mistakes, please let me know. There are bound to be several. Don’t worry though, after I gain more experience writing, my writing will improve and I’ll and catch more of the details I may have missed this time.

UPDATE: I added openid support and you can now download the app from this post

Tags: , ,