Rails 3 and AuthLogic: Basic Setup

Posted by joel Mon, 14 Mar 2011 01:14:00 GMT

Honestly, I was hard-pressed to find a complete guide that covered setting up Rails 3 with AuthLogic, including sending an email out for account activation. I'll try to cover as much detail as possible without going into too much detail (yes you read that right). I'm just going to go over the basic commands/files that you need to get the framework up and working. Please refer to the references for in depth information. The first one should be required reading. This is essentially an adaptation of the tutorial there, plus password resetting and a couple of other things. You may already have an application up, so you can skip the first few steps if necessary.

Grit the Round Hunnin

First off, obviously Rails 3 and Authlogic gems

$ [sudo] gem install rails authlogic

New app

$ rails new shut_your_face $ cd shut_your_face

Include the gem

# Gemfile gem "authlogic"

Models

UserSession

This is the model that will be in charge of ...uh... users' sessions. Absolutely necessary. As of this writing, the Authlogic generator ($ rails generate session user_session) doesn't work, so we'll just be creating a normal model, and then editing it so that it inherits from AuthLogic instead of ActiveRecord.

$ rails g model user_session

Now edit the model you just created to look like this:

class UserSession < Authlogic::Session::Base
end

I know, I know, it's really complicated. Authlogic does a lot of magic for you concerning sessions, which is one of the reasons that I love (read: less than three) it.

You're also going to have to edit the migration manually:

class CreateUserSessions < ActiveRecord::Migration
  def self.up
    create_table :sessions do |t|
      t.string :session_id, :null => false
      t.text :data
      t.timestamps
    end

    add_index :sessions, :session_id
    add_index :sessions, :updated_at
  end

  def self.down
    drop_table :sessions
  end
end

User

Generate the user model

$ rails g model user

Then, in the user model, you have to tell Authlogic that its the model that you wand to use for logging in and out.

class User < ActiveRecord::Base
  acts_as_authentic do |c|
  end # block optional
end

Now you can edit the migration using any of the fields in the documentation, I've included the ones that I used. Authlogic understands that if you don't use the login field, that you're going to want to use the email as the user's unique login id.

class CreateUsers < ActiveRecord::Migration
  def self.up
    create_table :users do |t|
      # t.string    :login,             :null => false                # optional, see below
      t.string    :crypted_password,    :null => false                # optional, see below
      t.string    :password_salt,       :null => false                # optional, but highly recommended
      t.string    :email,               :null => false                # optional, you can use login instead, or both
      t.string    :persistence_token,   :null => false                # required
      t.string    :single_access_token, :null => false                # optional, see Authlogic::Session::Params
      t.string    :perishable_token,    :null => false                # optional, see Authlogic::Session::Perishability
      
      # Magic columns, just like ActiveRecord's created_at and updated_at. These are automatically maintained by Authlogic if they are present.
      t.integer   :login_count,         :null => false, :default => 0 # optional, see Authlogic::Session::MagicColumns
      t.integer   :failed_login_count,  :null => false, :default => 0 # optional, see Authlogic::Session::MagicColumns
      t.datetime  :last_request_at                                    # optional, see Authlogic::Session::MagicColumns
      t.datetime  :current_login_at                                   # optional, see Authlogic::Session::MagicColumns
      t.datetime  :last_login_at                                      # optional, see Authlogic::Session::MagicColumns
      t.string    :current_login_ip                                   # optional, see Authlogic::Session::MagicColumns
      t.string    :last_login_ip                                      # optional, see Authlogic::Session::MagicColumns
      
      t.string    :name,                :null => false, :default => ''
      t.timestamps
    end
  end

  def self.down
    drop_table :users
  end
end

Migrate

$ rake db:migrate

ApplicationController helper methods

These are essential, as they are what will help with restricting access, seeing if a user if logged in, and returning the currently logged in user.

class ApplicationController < ActionController::Base
  protect_from_forgery
  
  filter_parameter_logging :password, :password_confirmation # there are underscores :-|
  helper_method :current_user_session, :current_user

  private
    def current_user_session
      return @current_user_session if defined?(@current_user_session)
      @current_user_session = UserSession.find
    end

    def current_user
      return @current_user if defined?(@current_user)
      @current_user = current_user_session && current_user_session.user
    end
    
    def require_user
      unless current_user
        store_location
        flash[:notice] = "You must be logged in to access this page"
        redirect_to new_user_session_url
        return false
      end
    end

    def require_no_user
      if current_user
        store_location
        flash[:notice] = "You must be logged out to access this page"
        redirect_to account_url
        return false
      end
    end

    def store_location
      session[:return_to] = request.request_uri
    end

    def redirect_back_or_default(default)
      redirect_to(session[:return_to] || default)
      session[:return_to] = nil
    end
end

The user_sessions controller, view, and routes

First, your UserSessions controller. I've included code pertinent to where we are now, and we'll be adding more later for additional features.

controller

$ rails g controller user_sessions
class UserSessionsController < ApplicationController
  before_filter :require_no_user, :only => [:new, :create]
  before_filter :require_user, :only => :destroy

  def new
    @user_session = UserSession.new
  end
  
  def create
    @user_session = UserSession.new(params[:user_session])
    if @user_session.save
      flash[:notice] = "Login successful!"
      redirect_back_or_default account_url(@current_user)
    else
      render :action => :new
    end
  end

  def destroy
    current_user_session.destroy
    flash[:notice] = "Logout successful!"
    redirect_back_or_default new_user_session_url
  end
end

In a gnutshell, 'new' is for the login form page, 'create' is where it submits, and 'destroy' is for the logout action. Gnuff said.

Views -s

# app/views/user_sessions/new.html.erb
<h1>Login</h1>

<% form_for @user_session, :as => :user_session, :url => { :action => "create" } do |f| %>
  <%= render "shared/error_messages", :target => @user_session %>
  <%= f.label :email %><br />
  <%= f.text_field :email %><br />
  <br />
  <%= f.label :password %><br />
  <%= f.password_field :password %><br />
  <br />
  <%= f.check_box :remember_me %><%= f.label :remember_me %><br />
  <br />
  <%= f.submit "Login" %>
<% end %>

The reason we're not using a url helper in the form_for helper (would be ":url => user_session_path") is for some weird error that Rails whines about not having a 'show' method or some crap like that. See one of the references for more info.

Routeses

Just a few for now. As we add features, we'll be updating the routes.

resources :user_sessions
  
match 'login' => "user_sessions#new",      :as => :login
match 'logout' => "user_sessions#destroy", :as => :logout

Users and User Registration

The User controller

Here's where users will register and update their information.

$ rails g controller users
class UsersController < ApplicationController
  before_filter :require_no_user, :only => [:new, :create]
  before_filter :require_user, :only => [:show, :edit, :update]
  
  def new
    @user = User.new
  end

  def create
    @user = User.new(params[:user])
    
    # Saving without session maintenance to skip
    # auto-login which can't happen here because
    # the User has not yet been activated
    if @user.save
      flash[:notice] = "Your account has been created."
      redirect_to signup_url
    else
      flash[:notice] = "There was a problem creating you."
      render :action => :new
    end
    
  end

  def show
    @user = current_user
  end

  def edit
    @user = current_user
  end

  def update
    @user = current_user # makes our views "cleaner" and more consistent
    if @user.update_attributes(params[:user])
      flash[:notice] = "Account updated!"
      redirect_to account_url
    else
      render :action => :edit
    end
  end
end

Routes

resources :users  # give us our some normal resource routes for users
resource :user, :as => 'account'  # a convenience route

match 'signup' => 'users#new', :as => :signup

So far if you take a look at your routes, you should see this:

$ rake routes
    user_sessions GET    /user_sessions(.:format)          {:action=>"index", :controller=>"user_sessions"}
                  POST   /user_sessions(.:format)          {:action=>"create", :controller=>"user_sessions"}
 new_user_session GET    /user_sessions/new(.:format)      {:action=>"new", :controller=>"user_sessions"}
edit_user_session GET    /user_sessions/:id/edit(.:format) {:action=>"edit", :controller=>"user_sessions"}
     user_session GET    /user_sessions/:id(.:format)      {:action=>"show", :controller=>"user_sessions"}
                  PUT    /user_sessions/:id(.:format)      {:action=>"update", :controller=>"user_sessions"}
                  DELETE /user_sessions/:id(.:format)      {:action=>"destroy", :controller=>"user_sessions"}
            login        /login(.:format)                  {:action=>"new", :controller=>"user_sessions"}
           logout        /logout(.:format)                 {:action=>"destroy", :controller=>"user_sessions"}
            users GET    /users(.:format)                  {:action=>"index", :controller=>"users"}
                  POST   /users(.:format)                  {:action=>"create", :controller=>"users"}
         new_user GET    /users/new(.:format)              {:action=>"new", :controller=>"users"}
        edit_user GET    /users/:id/edit(.:format)         {:action=>"edit", :controller=>"users"}
             user GET    /users/:id(.:format)              {:action=>"show", :controller=>"users"}
                  PUT    /users/:id(.:format)              {:action=>"update", :controller=>"users"}
                  DELETE /users/:id(.:format)              {:action=>"destroy", :controller=>"users"}
          account POST   /user(.:format)                   {:action=>"create", :controller=>"users"}
      new_account GET    /user/new(.:format)               {:action=>"new", :controller=>"users"}
     edit_account GET    /user/edit(.:format)              {:action=>"edit", :controller=>"users"}
                  GET    /user(.:format)                   {:action=>"show", :controller=>"users"}
                  PUT    /user(.:format)                   {:action=>"update", :controller=>"users"}
                  DELETE /user(.:format)                   {:action=>"destroy", :controller=>"users"}

Notice the "account" has the same routes as the user, just a different helper name. In the url, it will also show as "user".

Views

# app/views/users/_form.html.erb
<%= render "shared/error_messages", :target => @user %>

<p>
  <%= form.label :name %><br />
  <%= form.text_field :name %>
</p>
<p>
  <%= form.label :email %><br />
  <%= form.text_field :email %>
</p>
<p>
  <%= form.label :password, form.object.new_record? ? nil : "Change password" %><br />
  <%= form.password_field :password %>
</p>
<p>
  <%= form.label :password_confirmation %><br />
  <%= form.password_field :password_confirmation %>
</p>


# app/views/users/new.html.erb
<h1>Register</h1>

<% form_for @user do |f| %>
  <%= render :partial => "form", :object => f, :locals => { :user => @user } %>
  <%= f.submit "Register" %>
<% end %>


# app/views/users/edit.html.erb
<h1>Edit My Account</h1>

<% form_for @user, :url => account_path do |f| %>
  <%= render :partial => "form", :object => f, :locals => { :user => @user } %>
  <%= f.submit "Update" %>
<% end %>

<br /><%= link_to "My Profile", account_path %>


# app/views/users/show.html.erb
<p>
  <b>Email:</b>
  <%=h @user.email %>
</p>

<p>
  <b>Login count:</b>
  <%=h @user.login_count %>
</p>

<p>
  <b>Last request at:</b>
  <%=h @user.last_request_at %>
</p>

<p>
  <b>Last login at:</b>
  <%=h @user.last_login_at %>
</p>

<p>
  <b>Current login at:</b>
  <%=h @user.current_login_at %>
</p>

<p>
  <b>Last login ip:</b>
  <%=h @user.last_login_ip %>
</p>

<p>
  <b>Current login ip:</b>
  <%=h @user.current_login_ip %>
</p>

<%= link_to 'Edit', edit_account_path %>


# app/views/shared/_error_messages.html.cfm
<% if target.errors.any? %>
  <div id="errorExplanation">
    <h2><%= pluralize(target.errors.count, "error") %> prohibited this record from being saved:</h2>
    <ul>
      <% target.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
    </ul> 
  </div>
<% end %>

Works?

$ rails server

Hit /signup and sign up for an account! Then log out by going to /logout, and try logging back in at /login

There's more to come! In the next post, I'll be covering a few other things that are essential for normal web apps, namely: account activation via email authorization and the ever-helpful forgot password.

References

Posted in | 1 comment |

Comments

Leave a comment

  1. Avatar
    coredump
    5 days later:

    will try my hands on authlogic soon, looking forward to your upcoming posts. thanks

Leave a comment