Rails 3 and AuthLogic: Basic Setup
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
- The majority of the content was skeefed from the Authlogic tutorial on github.
- error messages: http://asciicasts.com/episodes/211-validations-in-rails-3
- little bug with form: http://techoctave.com/c7/posts/37-authlogic-and-rails-3-0-solution
5 days later:
will try my hands on authlogic soon, looking forward to your upcoming posts. thanks