241

Re: Restful Authentication with all the bells and whistles (new 9/05/08)

I seem to have worked out a solution for now. It's not the one I would like - that would be one that only sends the user to a kill session method when they have being deactivated, rather than always checking current_user.enabled? in the before_filter. I couldn't work that one out without spending more time on it than I want to, so, I worked out that I can use my old method, I just needed to include some conditionals to make sure that the logout procedure didn't break because the kill_session was always expecting to find a params[:user_id];

    def authorized?(action=nil, resource=nil, *args)
      logged_in? && current_user.enabled?
    end   

#
#
#

    def access_denied
      respond_to do |format|
        format.html do
          store_location   
          if current_user && !current_user.enabled?
            logout_killing_session!     
          end
          redirect_to login_path
        end
        # format.any doesn't work in rails version < http://dev.rubyonrails.org/changeset/8987
        # you may want to change format.any to e.g. format.any(:js, :xml)
        format.any do
          request_http_basic_authentication 'Web Password'
        end
      end
    end


That's probably as hackish as it gets - but it seems to work.

Re: Restful Authentication with all the bells and whistles (new 9/05/08)

ramon.tayag wrote:

I'm getting this "undefined method 'relative_url_root' for SessionsController:Class" error when trying to implement OpenID and I'm not sure where it's coming from.  I tried tinkering with it too but I didn't know what I was doing.

This happens when I'm in login and I type my OpenID URL and press submit:
http://pastie.org/263542

I didn't get the plugin from the link posted up there though, I got it from:
http://github.com/rails/open_id_authent … ree/master

What should I do?

Thanks

This error recently started showing up for me as well. relative_url_root is defined in ActionController::AbstractRequest, so there is no reason for it to be undefined. If anyone knows or can figure out why this is happening, it would be really helpful.

As a temporary fix, I created a file called request_error.rb in config/initializers with the following code (copied from ActionController::AbstractRequest):

class ActionController::CgiRequest
    def relative_url_root
      @@relative_url_root ||= case
        when @env["RAILS_RELATIVE_URL_ROOT"]
          @env["RAILS_RELATIVE_URL_ROOT"]
        when server_software == 'apache'
          @env["SCRIPT_NAME"].to_s.sub(/\/dispatch\.(fcgi|rb|cgi)$/, '')
        else
          ''
      end
    end
end

Once you add this file to the initializers you should stop getting the error.

Re: Restful Authentication with all the bells and whistles (new 9/05/08)

noob to ror here.

i have successfully implemented your system; thanks so much for your efforts on this.

one question about deployment config.

in the 'user_mailer.rb' file it seems we'd might need to reconfig the domain settings 'http://localhost:3000/' in production ... maybe i just don't get something magic about rails, but would there not be better way to make this a variable in the environment config files?

again ...i am probably not getting something basic here .. appreciate any comments.

Re: Restful Authentication with all the bells and whistles (new 9/05/08)

spherop wrote:

noob to ror here.

i have successfully implemented your system; thanks so much for your efforts on this.

one question about deployment config.

in the 'user_mailer.rb' file it seems we'd might need to reconfig the domain settings 'http://localhost:3000/' in production ... maybe i just don't get something magic about rails, but would there not be better way to make this a variable in the environment config files?

again ...i am probably not getting something basic here .. appreciate any comments.

Sure, constants such as that are best suited for a config file. A config file is being included in the nest version of the tutorial.

Re: Restful Authentication with all the bells and whistles (new 9/05/08)

cool - i just wanted to confirm what i 'thought' i might understand .. thx for response.

Re: Restful Authentication with all the bells and whistles (new 9/05/08)

Original restful authentication tutorial, new tutorial based on the latest version of the restful authentication plugin is on the first page.



This tutorial was built and tested using rails version 2.0.2. Features covered include activation, changing passwords, forgotten passwords, enable/disable users, roles and OpenID.

#Updated 2/21/08 to work with version 2+ of the ruby-openid gem and open_id_authentication plugin.

First generate a new project and install restful authentication:

rails myproject -d mysql
cd myproject
ruby script/plugin install http://svn.techno-weenie.net/projects/p … ntication/

Then, so that there is some sort of skeleton content management system, let's create a Page resource, and then run the code to generate the files used by the restful_authentication plugin:
ruby script/generate scaffold Page title:string body:text
ruby script/generate authenticated user sessions --include-activation

Make sure the name of the sessions_controller.rb is plural, as well as the sessions view folder. Also check that the line "include AuthenticatedSystem" was added to the application.rb file.

Next, create a file mail.rb in config/initializers (these are an example of my email settings, which work on Dreamhost):

# Email settings
ActionMailer::Base.delivery_method = :smtp
ActionMailer::Base.smtp_settings = {
  :address => "mail.yourapplication.com",
  :port => 25,
  :domain => "yourapplication.com",
  :authentication => :login,
  :user_name => "mail@yourapplication.com",
  :password => "yourapplicationpassword" 
}

Then in config/environment.rb, add the following line after "Rails::Initializer.run do |config|" so that the application sends email to the user after registration, activation, etc. (the user_observer.rb and user_mailer.rb files were generated by restful_authentication in app/models):
config.active_record.observers = :user_observer

Next, open user_mailer.rb in app/models and modify as desired (sample setting are below):
class UserMailer < ActionMailer::Base
  def signup_notification(user)
    setup_email(user)
    @subject    += 'Please activate your new account' 
    @body[:url]  = "http://localhost:3000/activate/#{user.activation_code}" 
  end
 
  def activation(user)
    setup_email(user)
    @subject    += 'Your account has been activated!'
    @body[:url]  = "http://localhost:3000/"
  end
 
  def forgot_password(user)
    setup_email(user)
    @subject    += 'You have requested to change your password'
    @body[:url]  = "http://localhost:3000/reset_password/#{user.password_reset_code}"
  end

  def reset_password(user)
    setup_email(user)
    @subject    += 'Your password has been reset.'
  end
 
  protected
    def setup_email(user)
      @recipients  = "#{user.email}"
      @from        = "mail@yourapplication.com"
      @subject     = "YourApplication - "
      @sent_on     = Time.now
      @body[:user] = user
    end
end


Then modify user_observer.rb to incorporate the reset and forgotten password features:
class UserObserver < ActiveRecord::Observer
  def after_create(user)
    UserMailer.deliver_signup_notification(user)
  end

  def after_save(user) 
    UserMailer.deliver_activation(user) if user.pending?
    UserMailer.deliver_forgot_password(user) if user.recently_forgot_password?
    UserMailer.deliver_reset_password(user) if user.recently_reset_password?
  end

end


Now let's start setting up roles/permissions:
ruby script/generate scaffold Role rolename:string
ruby script/generate model Permission

Modify the XXX_create_permissions.rb file in db/migrate:
class CreatePermissions < ActiveRecord::Migration
  def self.up
    create_table :permissions do |t|
      t.integer :role_id, :user_id, :null => false
      t.timestamps
    end
    #Make sure the role migration file was generated first   
    Role.create(:rolename => 'administrator')
    #Then, add default admin user
    #Be sure change the password later or in this migration file
    user = User.new
    user.login = "admin"
    user.email = "info@yourapplication.com"
    user.password = "admin"
    user.password_confirmation = "admin"
    user.save(false)
    user.send(:activate!)
    role = Role.find_by_rolename('administrator')
    user = User.find_by_login('admin')
    permission = Permission.new
    permission.role = role
    permission.user = user
    permission.save(false)
  end

  def self.down
      drop_table :permissions
      Role.find_by_rolename('administrator').destroy     
      User.find_by_login('admin').destroy     
  end
end


Before we start modifying the controllers, we also need to add enabled and password reset code columns to the XXX_create_users.rb migration file in db/migrate:
class CreateUsers < ActiveRecord::Migration
  def self.up
    create_table "users", :force => true do |t|
      t.column :login,                     :string
      t.column :email,                     :string
      t.column :crypted_password,          :string, :limit => 40
      t.column :salt,                      :string, :limit => 40
      t.column :created_at,                :datetime
      t.column :updated_at,                :datetime
      t.column :remember_token,            :string
      t.column :remember_token_expires_at, :datetime
      t.column :activation_code, :string, :limit => 40
      t.column :activated_at, :datetime
      t.column :password_reset_code, :string, :limit => 40
      t.column :enabled, :boolean, :default => true     
    end
  end

  def self.down
    drop_table "users"
  end
end


Next, we need to make some changes to the files in app/models.
First, model the has_many :through relationship of roles and users.
role.rb:
class Role < ActiveRecord::Base
  has_many :permissions
  has_many :users, :through => :permissions
end

permission.rb
class Permission < ActiveRecord::Base
  belongs_to :user
  belongs_to :role
end

Then, some changes need to be made to user.rb, namely a tweak to the email length validation, the user/roles relationship, a check to see if a user has a certain role, code for forgotten passwords, and some changes to the way a user is activated.
require 'digest/sha1'
class User < ActiveRecord::Base
  # Virtual attribute for the unencrypted password
  attr_accessor :password 

  validates_presence_of     :login, :email
  validates_presence_of     :password,                   :if => :password_required?
  validates_presence_of     :password_confirmation,      :if => :password_required?
  validates_length_of       :password, :within => 4..40, :if => :password_required?
  validates_confirmation_of :password,                   :if => :password_required?
  validates_length_of       :login,    :within => 3..40
  validates_length_of       :email,    :within => 6..100
  validates_uniqueness_of   :login, :email, :case_sensitive => false
  validates_format_of       :email, :with => /(^([^@\s]+)@((?:[-_a-z0-9]+\.)+[a-z]{2,})$)|(^$)/i

  has_many :permissions
  has_many :roles, :through => :permissions

  before_save :encrypt_password
  before_create :make_activation_code
 
  # prevents a user from submitting a crafted form that bypasses activation
  # anything else you want your user to change should be added here.
  attr_accessible :login, :email, :password, :password_confirmation

  class ActivationCodeNotFound < StandardError; end
  class AlreadyActivated < StandardError
    attr_reader :user, :message;
    def initialize(user, message=nil)
      @message, @user = message, user
    end
  end
 
  # Finds the user with the corresponding activation code, activates their account and returns the user.
  #
  # Raises:
  #  +User::ActivationCodeNotFound+ if there is no user with the corresponding activation code
  #  +User::AlreadyActivated+ if the user with the corresponding activation code has already activated their account
  def self.find_and_activate!(activation_code)
    raise ArgumentError if activation_code.nil?
    user = find_by_activation_code(activation_code)
    raise ActivationCodeNotFound if !user
    raise AlreadyActivated.new(user) if user.active?
    user.send(:activate!)
    user
  end

  def active?
    # the presence of an activation date means they have activated
    !activated_at.nil?
  end

  # Returns true if the user has just been activated.
  def pending?
    @activated
  end

  # Authenticates a user by their login name and unencrypted password.  Returns the user or nil.
  # Updated 2/20/08
  def self.authenticate(login, password)   
    u = find :first, :conditions => ['login = ?', login] # need to get the salt
    u && u.authenticated?(password) ? u : nil 
  end

  # Encrypts some data with the salt.
  def self.encrypt(password, salt)
    Digest::SHA1.hexdigest("--#{salt}--#{password}--")
  end

  # Encrypts the password with the user salt
  def encrypt(password)
    self.class.encrypt(password, salt)
  end

  def authenticated?(password)
    crypted_password == encrypt(password)
  end

  def remember_token?
    remember_token_expires_at && Time.now.utc < remember_token_expires_at
  end

  # These create and unset the fields required for remembering users between browser closes
  def remember_me
    remember_me_for 2.weeks
  end

  def remember_me_for(time)
    remember_me_until time.from_now.utc
  end

  def remember_me_until(time)
    self.remember_token_expires_at = time
    self.remember_token            = encrypt("#{email}--#{remember_token_expires_at}")
    save(false)
  end

  def forget_me
    self.remember_token_expires_at = nil
    self.remember_token            = nil
    save(false)
  end
 
  def forgot_password
    @forgotten_password = true
    self.make_password_reset_code
  end

  def reset_password
    # First update the password_reset_code before setting the
    # reset_password flag to avoid duplicate email notifications.
    update_attribute(:password_reset_code, nil)
    @reset_password = true
  end 

  #used in user_observer
  def recently_forgot_password?
    @forgotten_password
  end

  def recently_reset_password?
    @reset_password
  end
 
  def self.find_for_forget(email)
    find :first, :conditions => ['email = ? and activated_at IS NOT NULL', email]
  end
 
  def has_role?(rolename)
    self.roles.find_by_rolename(rolename) ? true : false
  end


  protected
 
  # before filter
  def encrypt_password
    return if password.blank?
    self.salt = Digest::SHA1.hexdigest("--#{Time.now.to_s}--#{login}--") if new_record?
    self.crypted_password = encrypt(password)
  end
     
  def password_required?
    crypted_password.blank? || !password.blank?
  end
   
  def make_activation_code
    self.activation_code = Digest::SHA1.hexdigest( Time.now.to_s.split(//).sort_by {rand}.join )
  end
   
  def make_password_reset_code
    self.password_reset_code = Digest::SHA1.hexdigest( Time.now.to_s.split(//).sort_by {rand}.join )
  end
   
  private
 
  def activate!
    @activated = true
    self.update_attribute(:activated_at, Time.now.utc)
  end   
       
end


I also recommend some changes for the file authenticated_system.rb, located in the lib directory, which was generated by the restful_authentication plugin. I added the methods not_logged_in_required, check_role, check_administrator_role, and permission_denied.
module AuthenticatedSystem
  protected
    # Returns true or false if the user is logged in.
    # Preloads @current_user with the user model if they're logged in.
    def logged_in?
      current_user != :false
    end

    # Accesses the current user from the session.  Set it to :false if login fails
    # so that future calls do not hit the database.
    def current_user
      @current_user ||= (login_from_session || login_from_basic_auth || login_from_cookie || :false)
    end

    # Store the given user id in the session.
    def current_user=(new_user)
      session[:user_id] = (new_user.nil? || new_user.is_a?(Symbol)) ? nil : new_user.id
      @current_user = new_user || :false
    end

    # Check if the user is authorized
    #
    # Override this method in your controllers if you want to restrict access
    # to only a few actions or if you want to check if the user
    # has the correct rights.
    #
    # Example:
    #
    #  # only allow nonbobs
    #  def authorized?
    #    current_user.login != "bob"
    #  end
    def authorized?
      logged_in?
    end

    # Filter method to enforce a login requirement.
    #
    # To require logins for all actions, use this in your controllers:
    #
    #   before_filter :login_required
    #
    # To require logins for specific actions, use this in your controllers:
    #
    #   before_filter :login_required, :only => [ :edit, :update ]
    #
    # To skip this in a subclassed controller:
    #
    #   skip_before_filter :login_required
    #
    def login_required
      authorized? || access_denied
    end
   
    def not_logged_in_required
      !logged_in? || permission_denied
    end
   
    def check_role(role)
      unless logged_in? && @current_user.has_role?(role)
        if logged_in?
          permission_denied
        else
          store_referer
          access_denied
        end
      end
    end

    def check_administrator_role
      check_role('administrator')
    end   

    # Redirect as appropriate when an access request fails.
    #
    # The default action is to redirect to the login screen.
    #
    # Override this method in your controllers if you want to have special
    # behavior in case the user is not authorized
    # to access the requested action.  For example, a popup window might
    # simply close itself.
    def access_denied
      respond_to do |format|
        format.html do
          store_location
          flash[:error] = "You must be logged in to access this feature."
          redirect_to :controller => '/session', :action => 'new'
        end
        format.xml do
          request_http_basic_authentication 'Web Password'
        end
      end
    end
   
    def permission_denied     
      respond_to do |format|
        format.html do
          #Put your domain name here ex. http://www.example.com
          domain_name = "http://localhost:3000"
          http_referer = session[:refer_to]
          if http_referer.nil?
            store_referer
            http_referer = ( session[:refer_to] || domain_name )
          end
          flash[:error] = "You don't have permission to complete that action."
          #The [0..20] represents the 21 characters in http://localhost:3000
          #You have to set that to the number of characters in your domain name
          if http_referer[0..20] != domain_name   
            session[:refer_to] = nil
            redirect_to root_path
          else
            redirect_to_referer_or_default(root_path)   
          end
        end
        format.xml do
          headers["Status"]           = "Unauthorized"
          headers["WWW-Authenticate"] = %(Basic realm="Web Password")
          render :text => "You don't have permission to complete this action.", :status => '401 Unauthorized'
        end
      end
    end

    # Store the URI of the current request in the session.
    #
    # We can return to this location by calling #redirect_back_or_default.
    def store_location
      session[:return_to] = request.request_uri
    end

    def store_referer
      session[:refer_to] = request.env["HTTP_REFERER"]
    end

    # Redirect to the URI stored by the most recent store_location call or
    # to the passed default.
    def redirect_back_or_default(default)
      redirect_to(session[:return_to] || default)
      session[:return_to] = nil
    end

    def redirect_to_referer_or_default(default)
      redirect_to(session[:refer_to] || default)
      session[:refer_to] = nil
    end

    # Inclusion hook to make #current_user and #logged_in?
    # available as ActionView helper methods.
    def self.included(base)
      base.send :helper_method, :current_user, :logged_in?
    end

    # Called from #current_user.  First attempt to login by the user id stored in the session.
    def login_from_session
      self.current_user = User.find(session[:user_id]) if session[:user_id]
    end

    # Called from #current_user.  Now, attempt to login by basic authentication information.
    def login_from_basic_auth
      authenticate_with_http_basic do |username, password|
        self.current_user = User.authenticate(username, password)
      end
    end

    # Called from #current_user.  Finaly, attempt to login by an expiring token in the cookie.
    def login_from_cookie
      user = cookies[:auth_token] && User.find_by_remember_token(cookies[:auth_token])
      if user && user.remember_token?
        user.remember_me
        cookies[:auth_token] = { :value => user.remember_token, :expires => user.remember_token_expires_at }
        self.current_user = user
      end
    end
end


The permission_denied method is definitly an area I feel needs improvement. Its just something I threw together so that users would be redirected properly if they tried to access a resource they didn't have permission for. Its designed to redirect back to the last page they were on, unless that page is on another site or has the same address as the resource they're trying to access. Please let me know if you have any suggested improvements for that method.

Now, let's modify the controllers. I moved a lot of the actions generated by restful_authentication into their own controllers in order to follow restful conventions as close as possible and in case I wanted to extend the functionality in the future.

First, the users_controller.rb in app/controllers:

class UsersController < ApplicationController
  layout 'application'
  before_filter :not_logged_in_required, :only => [:new, :create]   
  before_filter :login_required, :only => [:show, :edit, :update]
  before_filter :check_administrator_role, :only => [:index, :destroy, :enable]
 
  def index
      @users = User.find(:all)
  end
 
  #This show action only allows users to view their own profile
  def show
      @user = current_user
  end
   
  # render new.rhtml
  def new
    @user = User.new
  end

  def create
    cookies.delete :auth_token
    @user = User.new(params[:user])
    @user.save!
    #Uncomment to have the user logged in after creating an account - Not Recommended
    #self.current_user = @user
    flash[:notice] = "Thanks for signing up! Please check your email to activate your account before logging in."
    redirect_to login_path   
  rescue ActiveRecord::RecordInvalid
    flash[:error] = "There was a problem creating your account."
    render :action => 'new'
  end
 
  def edit
    @user = current_user
  end
 
  def update
    @user = User.find(current_user)
    if @user.update_attributes(params[:user])
      flash[:notice] = "User updated"
      redirect_to :action => 'show', :id => current_user
    else
      render :action => 'edit'
    end
  end
 
  def destroy
    @user = User.find(params[:id])
    if @user.update_attribute(:enabled, false)
      flash[:notice] = "User disabled"
    else
      flash[:error] = "There was a problem disabling this user."
    end
    redirect_to :action => 'index'
  end

  def enable
    @user = User.find(params[:id])
    if @user.update_attribute(:enabled, true)
      flash[:notice] = "User enabled"
    else
      flash[:error] = "There was a problem enabling this user."
    end
      redirect_to :action => 'index'
  end

end


Then, sessions_controller.rb (setting up the controller this way will make it easier to integrate openid authentication):
# This controller handles the login/logout function of the site.  
class SessionsController < ApplicationController
  layout 'application'
  before_filter :login_required, :only => :destroy
  before_filter :not_logged_in_required, :only => [:new, :create]
 
  # render new.rhtml
  def new
  end

  def create
    password_authentication(params[:login], params[:password])
  end

  def destroy
    self.current_user.forget_me if logged_in?
    cookies.delete :auth_token
    reset_session
    flash[:notice] = "You have been logged out."
    redirect_to login_path   
  end
 
  protected
 
  # Updated 2/20/08
  def password_authentication(login, password)
    user = User.authenticate(login, password)
    if user == nil
      failed_login("Your username or password is incorrect.")
    elsif user.activated_at.blank?   
      failed_login("Your account is not active, please check your email for the activation code.")
    elsif user.enabled == false
      failed_login("Your account has been disabled.")
    else
      self.current_user = user
      successful_login
    end
  end
 
  private
 
  def failed_login(message)
    flash.now[:error] = message
    render :action => 'new'
  end
 
  def successful_login
    if params[:remember_me] == "1"
      self.current_user.remember_me
      cookies[:auth_token] = { :value => self.current_user.remember_token , :expires => self.current_user.remember_token_expires_at }
    end
      flash[:notice] = "Logged in successfully"
      return_to = session[:return_to]
      if return_to.nil?
        redirect_to user_path(self.current_user)
      else
          redirect_to return_to
      end
  end

end


Next, generate two more controllers:
ruby script/generate controller Passwords
ruby script/generate controller Accounts

passwords_controller.rb:
class PasswordsController < ApplicationController
  layout 'application'
  before_filter :not_logged_in_required, :only => [:new, :create]
 
  # Enter email address to recover password   
  def new
  end
   
  # Forgot password action
  def create   
    return unless request.post?
    if @user = User.find_for_forget(params[:email])
      @user.forgot_password
      @user.save     
      flash[:notice] = "A password reset link has been sent to your email address."
      redirect_to login_path
    else
      flash[:notice] = "Could not find a user with that email address."
      render :action => 'new'
    end       
  end
 
  # Action triggered by clicking on the /reset_password/:id link recieved via email
  # Makes sure the id code is included
  # Checks that the id code matches a user in the database
  # Then if everything checks out, shows the password reset fields
  def edit
    if params[:id].nil?
      render :action => 'new'
      return
    end
    @user = User.find_by_password_reset_code(params[:id]) if params[:id]
    raise if @user.nil?
  rescue
    logger.error "Invalid Reset Code entered."
    flash[:notice] = "Sorry - That is an invalid password reset code. Please check your code and try again. (Perhaps your email client inserted a carriage return?)"
    #redirect_back_or_default('/')
    redirect_to new_user_path
  end
   
  # Reset password action /reset_password/:id
  # Checks once again that an id is included and makes sure that the password field isn't blank
  def update
    if params[:id].nil?
      render :action => 'new'
      return
    end
    if params[:password].blank?
      flash[:notice] = "Password field cannot be blank."
      render :action => 'edit', :id => params[:id]
      return
    end
    @user = User.find_by_password_reset_code(params[:id]) if params[:id]
    raise if @user.nil?
    return if @user unless params[:password]
      if (params[:password] == params[:password_confirmation])
        #Uncomment and comment lines with @user to have the user logged in after reset - not recommended
        #self.current_user = @user #for the next two lines to work
        #current_user.password_confirmation = params[:password_confirmation]
        #current_user.password = params[:password]
        #@user.reset_password
        #flash[:notice] = current_user.save ? "Password reset" : "Password not reset"
        @user.password_confirmation = params[:password_confirmation]
        @user.password = params[:password]
        @user.reset_password       
        flash[:notice] = @user.save ? "Password reset." : "Password not reset."
      else
        flash[:notice] = "Password mismatch."
        render :action => 'edit', :id => params[:id]
        return
      end 
      redirect_to login_path
  rescue
    logger.error "Invalid Reset Code entered"
    flash[:notice] = "Sorry - That is an invalid password reset code. Please check your code and try again. (Perhaps your email client inserted a carriage return?)"
    redirect_to new_user_path
  end
   
end

accounts_controller.rb:
class AccountsController < ApplicationController
  layout 'application'
  before_filter :login_required, :except => :show
  before_filter :not_logged_in_required, :only => :show

  # Activate action
  def show
    # Uncomment and change paths to have user logged in after activation - not recommended
    #self.current_user = User.find_and_activate!(params[:id])
    User.find_and_activate!(params[:id])
    flash[:notice] = "Your account has been activated! You can now login."
    redirect_to login_path
  rescue User::ArgumentError
    flash[:notice] = 'Activation code not found. Please try creating a new account.'
    redirect_to new_user_path   
  rescue User::ActivationCodeNotFound
    flash[:notice] = 'Activation code not found. Please try creating a new account.'
    redirect_to new_user_path
  rescue User::AlreadyActivated
    flash[:notice] = 'Your account has already been activated. You can log in below.'
    redirect_to login_path
  end
   
  def edit
  end
 
  # Change password action 
  def update
    return unless request.post?
    if User.authenticate(current_user.login, params[:old_password])
      if ((params[:password] == params[:password_confirmation]) && !params[:password_confirmation].blank?)
        current_user.password_confirmation = params[:password_confirmation]
        current_user.password = params[:password]       
        if current_user.save
          flash[:notice] = "Password successfully updated."
          redirect_to root_path #profile_url(current_user.login)
        else
          flash[:error] = "An error occured, your password was not changed."
          render :action => 'edit'
        end
      else
        flash[:error] = "New password does not match the password confirmation."
        @old_password = params[:old_password]
        render :action => 'edit'     
      end
    else
      flash[:error] = "Your old password is incorrect."
      render :action => 'edit'
    end   
  end
 
end


roles_controller.rb
class RolesController < ApplicationController
  layout 'application'
  before_filter :check_administrator_role

  def index
    @user = User.find(params[:user_id])
    @all_roles = Role.find(:all)
  end

  def update
    @user = User.find(params[:user_id])
    @role = Role.find(params[:id])
    unless @user.has_role?(@role.rolename)
      @user.roles << @role
    end
    redirect_to :action => 'index'
  end
 
  def destroy
    @user = User.find(params[:user_id])
    @role = Role.find(params[:id])
    if @user.has_role?(@role.rolename)
      @user.roles.delete(@role)
    end
    redirect_to :action => 'index'
  end

end


Now, the views. The first thing I would recommend is to create an application wide layout, that includes at least:
<ul>
    <% if logged_in? %>
      <li>Logged in as:</li>
      <li><%= link_to h(current_user.login.capitalize), user_path(current_user) %></li>
      <ul>
      <li><%= link_to 'Edit Profile', edit_user_path(current_user) %></li>
      <li><%= link_to 'Change Password', change_password_path %></li>
      <li><%= link_to 'Log Out', logout_url %></li>
      </ul>
      <% if current_user.has_role?('administrator') %>
        <li><%= link_to 'Administer Users', users_path %></li>
      <% end %>
    <% else %>
      <li><%= link_to 'Log In', new_session_path %></li>
      <li><%= link_to 'Sign Up', new_user_path %></li>     
      <li><%= link_to 'Forgot Password?', forgot_password_path %></li>     
    <% end %>        
</ul>

app/views/passwords/edit.html.erb:
<% form_tag url_for(:action => "update", :id => params[:id]) do %>
    Password:<br />
    <%= password_field_tag :password %><br />
    Confirm Password:<br />
    <%= password_field_tag :password_confirmation %><br />
    <%= submit_tag "Reset Your Password" %>
<% end %>

app/views/passwords/new.html.erb:
<h2>Forgot Password</h2>
<% form_tag url_for(:action => 'create') do %>
    What is the email address used to create your account?<br />
    <%= text_field_tag :email, "", :size => 50 %><br />
    <%= submit_tag 'Reset Password' %>
<% end %>

app/views/accounts/edit.html.erb:
<% form_tag url_for(:action => "update") do %>
  <p><label for="old_password" class="block">Old Password</label><br />
  <%= password_field_tag 'old_password', @old_password, :size => 45 %></p>

  <p><label for="password" class="block">New Password</label><br />
  <%= password_field_tag 'password', {}, :size => 45 %><br />
  <small>Between 4 and 40 characters</small></p>

  <p><label for="password_confirmation"  class="block">Confirm new password</label><br />
  <%= password_field_tag 'password_confirmation', {}, :size => 45 %></p>

  <%= submit_tag 'Change password' %>

<% end %>


app/views/roles/_role.html.erb:
<li>
  <%= role.rolename %>
    <% if @user.has_role?(role.rolename) %>
      <%= link_to 'remove role', user_role_url(:id => role.id, :user_id => @user.id), :method => :delete %>
    <% else %>
      <%= link_to 'assign role', user_role_url(:id => role.id, :user_id => @user.id), :method => :put %>
    <% end %>
</li>

app/views/roles/index.html.erb:
<h2>Roles for <%=h @user.login.capitalize %></h2>

<h3>Roles assigned:</h3>
<ul><%= render :partial => 'role', :collection => @user.roles %></ul>

<h3>Roles available:</h3>
<ul><%= render :partial => 'role', :collection => (@all_roles - @user.roles) %></ul>


You can remove or extend the other views in the roles folder.

app/views/session/new.html.erb:

<h2>Login with User ID and Password:</h2>
<% form_tag session_path do %>
<p><label for="login">Login</label><br/>
<%= text_field_tag 'login' %></p>

<p><label for="password">Password</label><br />
<%= password_field_tag 'password' %></p>


<p><label for="remember_me">Remember me:</label>
<%= check_box_tag 'remember_me' %></p>

<p><%= submit_tag 'Log in' %><%= link_to "Sign Up", new_user_path %></p>
<% end %>


app/views/user_mailer/activation.html.erb:
<%=h @user.login %>, your account has been activated.  To visit the site, follow the link below:

  <%= @url %>


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:
<%=h @user.login %>, Your password has been reset

app/views/user_mailer/signup_notification.html.erb:
Your account has been created.

  Username: <%=h @user.login %>

Visit this url to activate your account:

  <%= @url %>


app/views/users/_user.html.erb
<tr class="<%= cycle('odd', 'even') %>">
  <td><%=h user.login %></td>
  <td><%=h user.email %></td>
  <td><%= user.enabled ? 'yes' : 'no' %>
    <% unless user == current_user %>
      <% if user.enabled %>
        <%= link_to('disable', user_path(user.id), :method => :delete) %>
      <% else %>
        <%= link_to('enable', enable_user_path(user.id), :method => :put) %>
      <% end %>
    <% end %>
  </td>
  <td><%= link_to 'edit roles', user_roles_path(user) %>]</td>
</tr>

app/views/users/edit.html.erb:
<h2>Edit Your Account</h2>
<p><%= link_to 'Show Profile', user_path(@user) %> | <%= link_to 'Change Password', change_password_path %></p>
<%= error_messages_for :user %>

<% form_for :user, :url => user_url(@user), :html => { :method => :put } do |f| %>
  <p>Email:<br /><%= f.text_field :email, :size => 60 %></p>
 
<%= submit_tag 'Save' %>
<% end %>


app/views/users/index.html.erb:
<h2>All Users</h2>
<table>
  <tr>
    <th>Username</th>
    <th>Email</th>
    <th>Enabled?</th>
    <th>Roles</th>
  </tr>
    <%= render :partial => 'user', :collection => @users %>
</table>

app/views/users/new.html.erb:
<%= error_messages_for :user %>
<% form_for :user, :url => users_path do |f| %>
<p><label for="login">Login</label><br/>
<%= f.text_field :login %></p>

<p><label for="email">Email</label><br/>
<%= f.text_field :email %></p>

<p><label for="password">Password</label><br/>
<%= f.password_field :password %></p>

<p><label for="password_confirmation">Confirm Password</label><br/>
<%= f.password_field :password_confirmation %></p>

<p><%= submit_tag 'Sign up' %></p>
<% end %>


app/views/users/show.html.erb:
<h2>User: <%=h @user.login %></h2>
<p>Joined on: <%= @user.created_at.to_s(:long) %></p>

Finally, we need to modify the routes.rb in the config directory:
ActionController::Routing::Routes.draw do |map|
  map.root :controller => "pages", :action => "index"
  map.signup '/signup', :controller => 'users', :action => 'new'
  map.login '/login', :controller => 'sessions', :action => 'new'
  map.logout '/logout', :controller => 'sessions', :action => 'destroy'
  map.activate '/activate/:id', :controller => 'accounts', :action => 'show'
  map.forgot_password '/forgot_password', :controller => 'passwords', :action => 'new'
  map.reset_password '/reset_password/:id', :controller => 'passwords', :action => 'edit'
  map.change_password '/change_password', :controller => 'accounts', :action => 'edit'
 
  # See how all your routes lay out with "rake routes"
  map.resources :pages

  map.resources :users, :member => { :enable => :put } do |users|
      users.resource :account
      users.resources :roles
  end
 
  map.resource :session
  map.resource :password
 
  # Install the default routes as the lowest priority.
  map.connect ':controller/:action/:id'
  map.connect ':controller/:action/:id.:format'
end


Don't forget to delete index.html from the public directory.

To get the site up and running, we still need to create a database and run the migrations:

mysql -u root 
create database myproject_development;
exit

rake db:migrate


Start up the server and you should have a fully functioning user authentication system. Don't forget you have access to several before filters now, such as :login_required, :check_administrator_role, and :not_logged_in_required. Adding a new before filter is easy too, for example, you could add the following code to authenticated_system.rb to create a :check_moderator_role before filter. Make sure to add moderator to the roles table as well:
def check_moderator_role
  check_role('moderator')
end

Stop here if you don't want to use OpenID.

Now that the open_id_authentication plugin is being patched to work with verson 2+ of the ruby-openid gem, I have added this section on openid authentication to the tutorial:

To get started, install the latest version of the openid gem:

# Installs ruby-openid-2.0.4
gem install ruby-openid

To install the plugin, you can use the git repository at http://github.com/josh/open_id_authenti … y_openid_2
If you don't have or can't use git, you can download a rar file of it
from http://rapidshare.com/files/93608377/op … n.rar.html Unrar it and copy the folder into
vendor/plugins. I tried to put it on rubyforge so there would be SVN access but it takes 72 hours.

Run the migration for the open id authentication tables:

rake open_id_authentication:db:create

Open the migration file XXX_add_open_id_authentication, and add a column for the OpenID url in your users table:
add_column :users, :identity_url, :string

So that your migration file now looks like:
class AddOpenIdAuthenticationTables < ActiveRecord::Migration
  def self.up
    create_table :open_id_authentication_associations, :force => true do |t|
      t.integer :issued, :lifetime
      t.string :handle, :assoc_type
      t.binary :server_url, :secret
    end

    create_table :open_id_authentication_nonces, :force => true do |t|
      t.integer :timestamp, :null => false
      t.string :server_url, :null => true
      t.string :salt, :null => false
    end
   
     add_column :users, :identity_url, :string
  end
 


  def self.down
    remove_column :users, :identity_url
    drop_table :open_id_authentication_associations
    drop_table :open_id_authentication_nonces
  end
end


Run the migration:
rake db:migrate

Add the route for OpenID authentication to your routes file:
map.open_id_complete 'session', :controller => "sessions", :action => "create", 
                                :requirements => { :method => :get }

so that your routes.rb file looks like:
ActionController::Routing::Routes.draw do |map|
  map.root :controller => "pages", :action => "index"
  map.signup '/signup', :controller => 'users', :action => 'new'
  map.login '/login', :controller => 'sessions', :action => 'new'
  map.logout '/logout', :controller => 'sessions', :action => 'destroy'
  map.activate '/activate/:id', :controller => 'accounts', :action => 'show'
  map.forgot_password '/forgot_password', :controller => 'passwords', :action => 'new'
  map.reset_password '/reset_password/:id', :controller => 'passwords', :action => 'edit'
  map.change_password '/change_password', :controller => 'accounts', :action => 'edit'
  map.open_id_complete 'session', :controller => "sessions", :action => "create", :requirements => { :method => :get }

  # See how all your routes lay out with "rake routes"
  map.resources :pages

  map.resources :users, :member => { :enable => :put } do |users|
      users.resource :account
      users.resources :roles
  end
 
  map.resource :session
  map.resource :password 
 
  # Install the default routes as the lowest priority.
  map.connect ':controller/:action/:id'
  map.connect ':controller/:action/:id.:format'
end


Modify the attr_accesible line in your User model so that your users can set and change their OpenID:
attr_accessible :login, :email, :password, :password_confirmation, :identity_url

You also need to modify the password_required? method in the User model:
  def password_required?
    not_openid? && (crypted_password.blank? || !password.blank?)
  end

and add below it:
  def not_openid?
    identity_url.blank?
  end

Edit the New sessions view at sessions/new.html.erb to include an option for an OpenID login:
<h2>Login with User ID and Password:</h2>
<% form_tag session_path do -%>
<p><label for="login">Login</label><br/>
<%= text_field_tag 'login' %></p>

<p><label for="password">Password</label><br/>
<%= password_field_tag 'password' %><br />
<label for="remember_me">Remember me:</label>
<%= check_box_tag 'remember_me' %></p> 


<p><br /><label for="openid_url">Optional - use your OpenID url:</label><br />
<%= text_field_tag "openid_url" %></p>

<p><%= submit_tag 'Log In' %> or <%= link_to "Sign Up", new_user_path %></p>
<% end -%>


If you want the OpenID logo used in this form field, add the following code to your CSS file:
input#openid_url {
   background: url(http://openid.net/login-bg.gif) no-repeat;
   background-color: #fff;
   background-position: 0 50%;
   color: #000;
   padding-left: 18px;
}

Next, modify the Sessions Controller to include OpenID authentication. Here we have added if using_open_id? and the methods open_id_authentication and create_open_id_user:
class SessionsController < ApplicationController
  layout 'application'
  before_filter :login_required, :only => :destroy
  before_filter :not_logged_in_required, :only => [:new, :create]
 
  # render new.rhtml
  def new
  end

  def create
    if using_open_id?
      open_id_authentication(params[:openid_url])
    else 
      password_authentication(params[:login], params[:password])
    end 
  end

  def destroy
    self.current_user.forget_me if logged_in?
    cookies.delete :auth_token
    reset_session
    flash[:notice] = "You have been logged out."
    redirect_to login_path
    #redirect_back_or_default('/')
  end
 
  protected
 
  def open_id_authentication(openid_url)
    authenticate_with_open_id(openid_url, :required => [:nickname, :email]) do |result, identity_url, registration|
      if result.successful?
        @user = User.find_or_initialize_by_identity_url(identity_url)
        if @user.new_record?
          if !registration['nickname'].blank? && !registration['email'].blank?
            @user.login = registration['nickname']
            @user.email = registration['email']
            create_open_id_user(@user)
          else
            flash[:error] = "Your persona must include at a minimum a nickname
                             and valid email address to use OpenID on this site."
            render :action => 'new'
          end
        else
          if @user.activated_at.blank?   
            failed_login("Your account is not active, please check your email for the activation code.")
          elsif @user.enabled == false
            failed_login("Your account has been disabled.")
          else
            self.current_user = @user
            successful_login
          end       
        end
      else
        failed_login result.message
      end
    end
  end
 
  def create_open_id_user(user)
    user.save!
    flash[:notice] = "Thanks for signing up! Please check your email to activate your account before logging in."
    redirect_to login_path
  rescue ActiveRecord::RecordInvalid
    flash[:error] = "Someone has signed up with that nickname or email address. Please create
                             another persona for this site."
    render :action => 'new'
  end   

  def password_authentication(login, password)
    user = User.authenticate(login, password)
    if user == nil
      failed_login("Your username or password is incorrect.")
    elsif user.activated_at.blank?   
      failed_login("Your account is not active, please check your email for the activation code.")
    elsif user.enabled == false
      failed_login("Your account has been disabled.")
    else
      self.current_user = user
      successful_login
    end
  end
 
  private
 
  def failed_login(message)
    flash.now[:error] = message
    render :action => 'new'
  end
 
  def successful_login
    if params[:remember_me] == "1"
      self.current_user.remember_me
      cookies[:auth_token] = { :value => self.current_user.remember_token , :expires => self.current_user.remember_token_expires_at }
    end
      flash[:notice] = "Logged in successfully"
      return_to = session[:return_to]
      if return_to.nil?
        redirect_to user_path(self.current_user)
      else
          redirect_to return_to
      end
  end

end


Even though nickname and email are listed as required openid fields, the application was still allowing a new user to be created without them, so I had to !registration['nickname'].blank? && !registration['email'].blank? validation to make sure they were actually present. If anyone knows any more about this let me know. Also, I couldn't get the remember me checkbox to work with openid authentication because of the redirect to your openid provider. Please let me know if anyone has a fix for this as well.


References / Additional Resources:

Restful Authentication:
- http://svn.techno-weenie.net/projects/p … ntication/ Thank you, Rick Olson (techno-weenie.net), without this plugin I'd still be developing my login system.
Setup:
- http://railscasts.com/episodes/67
Modifications:
- http://www.carmelyne.com/2007/8/21/rest … ion-mailer
- http://technoweenie.stikipad.com/plugin … +passwords
- http://technoweenie.stikipad.com/plugin … +Resetting
- http://railsforum.com/viewtopic.php?id=11962
Mailer Modification:
- http://technoweenie.stikipad.com/plugin … iler+Setup
Activation Method Modification:
- http://technoweenie.stikipad.com/plugin … Activation
- http://toolmantim.com/article/2007/1/31 … activation
Routing Modifications:
- http://railsforum.com/viewtopic.php?id=13653
OpenID:
- http://railscasts.com/episodes/68
- http://github.com/josh/open_id_authenti … y_openid_2
- http://www.bencurtis.com/archives/2007/ … enticated/
- http://openidenabled.com/ruby-openid/
- http://svn.rubyonrails.org/rails/plugin … ion/README
- http://dev.rubyonrails.org/ticket/10604
Highly recommended additional reading:
- Apress Practical Rails Social Networking Sites (http://www.amazon.com/Practical-Rails-S … 1590598415). While Agile Web Development with Rails and The Rails Way are great books, this is my personal favorite; I felt I learned the most from this book. There are so many features covered that are easy to incorporate into your projects, and I liked the book more than RailsSpace because most of the code is restful.
Roles Plugin (not used in the tutorial):
- This is a promising plugin, but I like the has_many :through relationship used above more than the habtm relationship used in the plugin.
- http://code.google.com/p/rolerequirement/

Re: Restful Authentication with all the bells and whistles (new 9/05/08)

I would like to hear from anyone who has added an AVATAR system for users using the restful auth system.

If you have any pointer, link, plugin recs etc - please feel free to share.

248

Re: Restful Authentication with all the bells and whistles (new 9/05/08)

spherop wrote:

I would like to hear from anyone who has added an AVATAR system for users using the restful auth system.

If you have any pointer, link, plugin recs etc - please feel free to share.

Try Paperclip. I've just about finished replacing attachment_fu with Paperclip on user and three other models - it helped me to finish a whole load of features faster than if I had stayed on attachment_fu (the app has an rss parser which handles images; as far as I'm aware - and I did test this - attachment_fu would've needed an extra process for generating thumbnails in the background, Paperclip just thumbnailed and pushed to S3 with no problems whatsoever, and with far few models, database hits, and less code for the thumbnails).

http://jimneath.org/2008/04/17/papercli … -in-rails/

activefx, this rocks - I'm really looking forward to checking out how you've updated this to work with the latest version of RA. I came back to this tutorial a few weeks ago and I was a little confused as to what was still needed, e.g, aren't the password/cookie methods covered in the additional modules that rick olson has since added to RA?. An app I'm working on is seeing a 'no method user_id for <User::something>' when logging out whilst hitting the 'forget_me' method - I'm hoping to fix later but maybe it's something solved by your update.

One suggestion; wouldn't it be better to start a new thread for this? We'll be toing and froing to see the latest updates/comments.

249

Re: Restful Authentication with all the bells and whistles (new 9/05/08)

Hello,

I am an amateur ruby on rails programmer.   I have downloaded an unziped the file here:  http://github.com/activefx/restful_auth … ee/master.  I am running Windows.  How do I get this restful authentication integrated with a project?

Re: Restful Authentication with all the bells and whistles (new 9/05/08)

I'm running Rails 2.1.0 on Mac OS X Leopard, and install plugin didn't work for me until I commented out RAILS_GEM_VERSION in the environment.rb file.  I was following post #247 on this thread (the "original tutorial" one by Activefx on September 5th), and I'm stuck on this and don't know what to make of it:

$ ruby script/generate scaffold Role rolename:string
/Library/Ruby/Gems/1.8/gems/activesupport-2.1.0/lib/active_support/dependencies.rb:492:in `const_missing': uninitialized constant User::Authentication (NameError)
    from /Users/naijaguy/Sites/rails_apps/testauthapp/app/models/user.rb:4
    from /Library/Ruby/Gems/1.8/gems/activesupport-2.1.0/lib/active_support/dependencies.rb:215:in `load_without_new_constant_marking'
    from /Library/Ruby/Gems/1.8/gems/activesupport-2.1.0/lib/active_support/dependencies.rb:215:in `load_file'
    from /Library/Ruby/Gems/1.8/gems/activesupport-2.1.0/lib/active_support/dependencies.rb:354:in `new_constants_in'
    from /Library/Ruby/Gems/1.8/gems/activesupport-2.1.0/lib/active_support/dependencies.rb:214:in `load_file'
    from /Library/Ruby/Gems/1.8/gems/activesupport-2.1.0/lib/active_support/dependencies.rb:95:in `require_or_load'
    from /Library/Ruby/Gems/1.8/gems/activesupport-2.1.0/lib/active_support/dependencies.rb:260:in `load_missing_constant'
    from /Library/Ruby/Gems/1.8/gems/activesupport-2.1.0/lib/active_support/dependencies.rb:467:in `const_missing'
     ... 28 levels...
    from /Library/Ruby/Gems/1.8/gems/rails-2.1.0/lib/commands/generate.rb:1
    from /Library/Ruby/Site/1.8/rubygems/custom_require.rb:27:in `gem_original_require'
    from /Library/Ruby/Site/1.8/rubygems/custom_require.rb:27:in `require'
    from script/generate:3

It makes me think that when I installed the plugin something wasn't installed.  ApplicationController is looking pretty simple at the moment:

class ApplicationController < ActionController::Base
 
  include AuthenticatedSystem
 
  helper :all # include all helpers, all the time

  # See ActionController::RequestForgeryProtection for details
  # Uncomment the :secret if you're not using the cookie session store
  protect_from_forgery # :secret => '9026f586557c0b14bf1303c1d59f5d54'
end

Re: Restful Authentication with all the bells and whistles (new 9/05/08)

After I execute 'rake db:migrate' from the tutorial steps it fails like so:

# rake db:migrate --trace
(in /Users/briandunbar/rails/restful_authentication_tutorial)
** Invoke db:migrate (first_time)
** Invoke environment (first_time)
** Execute environment
** Execute db:migrate
==  SetUpFirstAdminUser: migrating ============================================
rake aborted!
An error has occurred, all later migrations canceled:

You have a nil object when you didn't expect it!
The error occurred while evaluating nil.activated_at=
./db/migrate//20080806025753_set_up_first_admin_user.rb:16:in `up_without_benchmarks'


Environment:
# ruby --version
ruby 1.8.6 (2008-03-03 patchlevel 114) [i686-darwin8.11.1]
# rails --version
Rails 2.1.0

Any idea what's up with that?

Re: Restful Authentication with all the bells and whistles (new 9/05/08)

hey all,
my ActionMailer is really acting up.. its so messed up and unreliable.. its working fine one minute and not working at all the next minute.. i have already posted my problems with it in posts 231,233 in this tutorial.. the problem i have with it now is the most perplexing.. everything gets done properly.. i can see that the mail is sent in the dvlpmnt log.. but i dont see the mail in my email inbox. I really need some clarity on this.. ive been checking and rechecking the mailer models, the observers, mail.rb.. all settings are just as they were when i WAS getting the emails in my inbox.. anyone at all listening? anyone got any thoughts at all on this?

Re: Restful Authentication with all the bells and whistles (new 9/05/08)

naijaguy wrote:

I'm running Rails 2.1.0 on Mac OS X Leopard, and install plugin didn't work for me until I commented out RAILS_GEM_VERSION in the environment.rb file.  I was following post #247 on this thread (the "original tutorial" one by Activefx on September 5th), and I'm stuck on this and don't know what to make of it:

It appears that you're using the latest version of the restful authentication plugin. You need to use the "classic" branch of that plugin if you want to follow the old tutorial. The new tutorial will cover the latest version.

Re: Restful Authentication with all the bells and whistles (new 9/05/08)

yourstruly_vinay wrote:

hey all,
my ActionMailer is really acting up.. its so messed up and unreliable.. its working fine one minute and not working at all the next minute.. i have already posted my problems with it in posts 231,233 in this tutorial.. the problem i have with it now is the most perplexing.. everything gets done properly.. i can see that the mail is sent in the dvlpmnt log.. but i dont see the mail in my email inbox. I really need some clarity on this.. ive been checking and rechecking the mailer models, the observers, mail.rb.. all settings are just as they were when i WAS getting the emails in my inbox.. anyone at all listening? anyone got any thoughts at all on this?

From your previous posts it sounds like you've made quite a few modifications to the tutorial, which makes it nearly impossible to provide support. Here are the only suggestions I can think of to help, since I don't have the full picture:

- Set up action mailer for a separate application and confirm that mails are being sent, ie make sure your mailer settings are correct.
- Follow the original tutorial as is and make sure all of the emails are being sent as anticipated, then follow a test/behavior driven development route as you slowly make modifications.
- It sounds like you created a account model which requires emails to be sent. Is there an observer for this model? Are the instance variables this observer needs to observe being properly set? Are the observers monitoring the correct items? Have the observers output to the development log instead of executing a mailer action.
- Make sure callbacks and filters aren't interfering with the observers and actionmailer. Its easy for them to cause duplicate emails or interfere with the sending of emails. Again, outputting to the development log could help diagnose problems.

Re: Restful Authentication with all the bells and whistles (new 9/05/08)

@activefx - thanks a lot! its just a huge relief to see a person with much more experience is listening. I have indeed made quite a few modifications to the tutorial. which was why even i dint know what part of the code to post up here. I will definitely try out your suggestions and try and figure it out through test driven dvlpmnt from scratch.

It sounds like you created a account model which requires emails to be sent. Is there an observer for this model? Are the instance variables this observer needs to observe being properly set? Are the observers monitoring the correct items? Have the observers output to the development log instead of executing a mailer action.

i have an observer for the model and it is set up in environment.rb alongwith the user_observer. I think everything else is fine because, like i said, the development log shows that the correct emails are sent to the correct email addresses. But they simply arent showing up in my inbox! Any idea what might be causing that specific problem?
am i right in thinking that when u say 'output into the development log', u mean that i have to write into the logger hash?

Re: Restful Authentication with all the bells and whistles (new 9/05/08)

@activefx - When do you think you will have the new tutorial up?  Waiting anxiously! smile

Re: Restful Authentication with all the bells and whistles (new 9/05/08)

nodabs wrote:

@activefx - When do you think you will have the new tutorial up?  Waiting anxiously! smile

Soon, but not quite as soon as I expected. You can follow the progress at http://github.com/activefx/restful_auth … ts/master. As soon as I've fixed enough of the initial problems and implemented all of the major features, I will post the tutorial.

Re: Restful Authentication with all the bells and whistles (new 9/05/08)

Awesome, looking forward to it.  I did some browsing through the code and already learned a lot from it.  Thanks for all the effort.

Re: Restful Authentication with all the bells and whistles (new 9/05/08)

Activefx, thank you for this!  I edited the roles_controller because it seemed that when I was removing roles from someone it was destroying the role too!  I don't remember this happening the first time I installed this, but here's the Roles controller I'm using.

class RolesController < ApplicationController
  layout 'application'

  # GET /roles
  # GET /roles.xml
  def index
    @user = User.find(params[:user_id])
    @roles = Role.all
    @available_roles = @roles - @user.roles

    respond_to do |format|
      format.html # index.html.erb
      format.xml  { render :xml => @roles }
    end
  end

  # GET /roles/1
  # GET /roles/1.xml
  def show
    @role = Role.find(params[:id])

    respond_to do |format|
      format.html # show.html.erb
      format.xml  { render :xml => @role }
    end
  end

  # GET /roles/new
  # GET /roles/new.xml
  def new
    @role = Role.new
    @user = User.find params[:user_id]

    respond_to do |format|
      format.html # new.html.erb
      format.xml  { render :xml => @role }
    end
  end

  # GET /roles/1/edit
  def edit
    @role = Role.find(params[:id])
    @user = User.find params[:user_id]
  end

  def create
    @role = Role.new(params[:role])
    @user = User.find params[:user_id]

    respond_to do |format|
      if @role.save
        flash[:notice] = 'Role was successfully created.'
        format.html { redirect_to(user_roles_path(@user) ) }
        format.xml  { render :xml => @role, :status => :created, :location => @role }
      else
        format.html { render :action => "new" }
        format.xml  { render :xml => @role.errors, :status => :unprocessable_entity }
      end
    end
  end

  def update
    @role = Role.find(params[:id])
    @user = User.find params[:user_id]

    respond_to do |format|
      if @role.update_attributes(params[:role])
        flash[:notice] = 'Role was successfully updated.'
        format.html { redirect_to(user_roles_path(@user) ) }
        format.xml  { render :xml => @role, :status => :created, :location => @role }
      else
        format.html { render :action => "new" }
        format.xml  { render :xml => @role.errors, :status => :unprocessable_entity }
      end
    end
  end

  def assign
    @user = User.find(params[:user_id])
    @role = Role.find(params[:id])

    @user.roles << @role unless @user.has_role?(@role)

    respond_to do |format|
      if @role.update_attributes(params[:role])
        flash[:notice] = 'Role was successfully updated.'
        format.html { redirect_to(user_roles_path(@user) ) }
        format.xml  { head :ok }
      else
        format.html { render :action => "edit" }
        format.xml  { render :xml => @role.errors, :status => :unprocessable_entity }
      end
    end
  end

  def unassign
    @user = User.find(params[:user_id])
    @role = Role.find(params[:id])
    @user.roles.delete(@role) if @user.has_role?(@role)

    respond_to do |format|
      format.html { redirect_to(user_roles_url @user) }
      format.xml  { head :ok }
    end
  end

  def destroy
    @user = User.find params[:user_id]
    role = Role.find params[:id]
    role.destroy

    respond_to do |format|
      flash[:notice] = "Successfully deleted the role"
      format.html { redirect_to(user_roles_url @user) }
      format.xml  { head :ok }
    end
  end
end


In my routes I have my roles resources set up like this:
  map.resources :users, :member => {:enable => :put} do |users|
    users.resource :account
    users.resources :roles, :member => {:assign => :put, :unassign => :put}
  end

Then to assign a user:
link_to 'remove role', assign_user_role_url(:id => role.id, :user_id => user.id), :method => :put

To unassign a user:
link_to 'remove role', unassign_user_role_url(:id => role.id, :user_id => user.id), :method => :put

Last edited by ramon.tayag (2008-09-14 03:43:31)

Re: Restful Authentication with all the bells and whistles (new 9/05/08)

activefx wrote:
ramon.tayag wrote:

I'm getting this "undefined method 'relative_url_root' for SessionsController:Class" error when trying to implement OpenID and I'm not sure where it's ....
Thanks

This error recently started showing up for me as well. relative_url_root is defined in ActionController::AbstractRequest, so there is no reason for it to be undefined. If anyone knows or can figure out why this is happening, it would be really helpful.

As a temporary fix, I created a file called request_error.rb in config/initializers with the following code (copied from ActionController::AbstractRequest):

hmm...i have the same error too. doing the request_error.rb error didn't help. For my app, the error is occuring when my YM4R-GM is trying to access the javascript file.

a << "<script src=\"#{ActionController::AbstractRequest.relative_url_root}/javascripts/ym4r-gm.js\" type=\"text/javascript\"></script>\n" unless options[:without_js]

I have no idea what is causing this error. the other pages work well and my original app(without restful auth) doesn't throw up any error.