Monday, March 10, 2008

Rails Forum - Restful Authentication(Part 3 of 3)

So here is part 3 of 3 for my Rails 2.0 Forum Tutorial for Beginners. Today we are going to put authentication on top of our application, using the restful_authentication and acts_as_state_machine plugins.

NOTE: If you don't want to do Parts 1 and 2, you can download the code for them here, and use this as a stand-alone tutorial. Please note that our Topic and Reply models both belongs_to a user.

I got a lot of this stuff from RubyPlus's RESTful Authentication tutorial. Let's get started!


cd myforum

ruby script/plugin install http://elitists.textdriven.com/svn/plugins/acts_as_state_machine/trunk
ruby script/plugin install http://svn.techno-weenie.net/projects/plugins/restful_authentication
ruby script/generate authenticated user sessions --include-activation --stateful


To break down that last command - authenticated is part of the restful_authentication. user is the model to be generated for the user. sessions will be the controller to handle logging in/out, --include-activation will help with creating the stuff needed to mail an activation link to the user's email, and --stateful is for support for acts_as_state_machine plugin.


Now that our files are generated, let's head to /config/routes.rb and make a couple of changes. Notice that the map.resources :users and map.resources :session lines have already been generated by the generate authenticated call. The version of restful_authentication used in the tutorial on RubyPlus did not automatically generate them.


So in routes.rb:



#CHANGE THIS LINE
map.resources :users

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

#AND THEN ADD THE FOLLOWING LINES
map.activate '/activate/:activation_code', :controller => 'users',
:action => 'activate'
map.signup '/signup', :controller => 'users', :action => 'new'
map.login '/login', :controller => 'sessions', :action => 'new'
map.logout '/logout', :controller => 'sessions', :action => 'destroy'
map.forgot_password '/forgot_password', :controller => 'users',
:action => 'forgot_password'
map.reset_password '/reset_password/:id', :controller => 'users',
:action => 'reset_password'

The :member => part sets up some restful actions for the users controller. All the lines that we added sets up named routes for user and session controller/action pairs.


Now, open up /config/environment.rb:



#BELOW THE LINE
Rails::Initializer.run do |config|

#ADD THIS
config.active_record.observers = :user_observer

The observer watches the user model and will be used to send an EMail whenever a new user is created. Next, lets open up /db/migrate/004_create_users.rb and do this.



#OLD 004_create_users.rb
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 :state, :string, :null => :no, :default => 'passive'
t.column :deleted_at, :datetime
end
end

def self.down
drop_table "users"
end
end

#NEW 004_create_users.rb
class CreateUsers < ActiveRecord::Migration
def self.up
create_table "users", :force => true do |t|
t.string :login, :email, :remember_token
t.string :crypted_password, :limit => 40
t.string :password_reset_code, :limit => 40
t.string :salt, :limit => 40
t.string :activation_code, :limit => 40
t.datetime :remember_token_expires_at, :activated_at, :deleted_at
t.string :state, :null => :no, :default => 'passive'

t.timestamps
end
end

def self.down
drop_table "users"
end
end


There we are just cleaning up the code a little bit. We did add a :password_reset_code column - will be used if a user forgets their password(more on that later). Now just run the migration to create our new tables...


rake db:migrate

Next, open up app/controllers/sessions_controller and copy the line include AuthenticatedSystem to /app/controllers/application.rb. Also, in users_controller, remove the same line. This will make sure that the restful_authentication functions are included on every page.


Now, remove these lines from users_controller in the create function:



self.current_user = @user
redirect_back_or_default('/')

We removed the first line because we do not want to log the user in when they register, we want to verify their EMail. The second line was removed so that Rails would look for create.html.erb after running the create action. Now, create a file /app/views/users/create.html.erb and put something like this in it:



<fieldset>
<legend>Your forum account</legend>

<p>
An EMail has been sent to <%= @user.email %> with instructions to activate
your account.
<li>
If your email is not valid, you must <%= link_to "signup", signup_path %>
again and provide a valid email address.
</li>
<li>
If you don't recieve an email, check your bulk or trash folder, as your spam
filter may have inadvertantly caught the registration email.
</li>
</p>
</fieldset>

Also, create the files app/views/users/change_password.html.erb, app/views/users/forgot_password.html.erb, app/views/users/reset_password.html.erb:



#change_password.html.erb

<% form_for :action => 'change_password' do |f| %>
<fieldset>
<dl>
<dt><label for="old_password" class="block">Old Password</label></dt>
<dd><%= password_field_tag 'old_password', @old_password, :size => 45, :class => 'text' %></dd>

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

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

</dl>
<%= submit_tag 'Change password' %>
</fieldset>
<%end%>

#forgot_password.html.erb

<% form_for :user, :url => {:action => 'forgot_password'} do |form| %>
<fieldset>
<legend>Password Reset Request</legend>
<p>Enter your email address that we have on our file and click send. We will send you a
password reset link email to your email address.</p>
<p>
<label for="user_email" >Email Address:</label><br/>
<%= form.text_field :email, :size => 35, :class => 'text' %>
</p>
<%= submit_tag 'Send' %>
</fieldset>
<% end %>

#reset_password.html.erb

<% form_for :user, :url => {:action => "reset_password"} do |form| %>
<fieldset>
<legend>Reset Password</legend>

<p>
<label for="user_password" >Password</label><br/>
<%= form.password_field :password, :size => 45, :class => 'text' %>
</p>
<p>
<label for="user_password_confirmation" >Confirm Password</label><br/>
<%= form.password_field :password_confirmation, :size => 45, :class => 'text' %>
</p>
<%= submit_tag "Reset your password" %>
</fieldset>
<% end %>
</pre>
<p>Standard stuff there.</p>
<p>Now, open up app/helpers/application_helper.rb and add this code:</p>
<pre>
def user_logged_in?
session[:user_id]
end

We can use this in our views to see if the user is logged in.


Create app/views/layouts/application.html.erb:



<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en-US">

<head>
<title><%= @page_title || 'Rails 2.0 Forum' %></title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<%= stylesheet_link_tag 'style' %>

<%= javascript_include_tag :defaults %>
</head>

<body>

<!-- ##### Header ##### -->
<div id="header">
<div class="superHeader">
<% if user_logged_in? %>
<span>You are logged in as: </span>
<%= current_user.login %>
<% else %>
<span>Welcome Guest</span>
<% end %>
</div>

<div class="midHeader">
<h1 class="headerTitle" lang="la">Rails 2.0 Forum</h1>
</div>

<div class="subHeader">
<span class="doNotDisplay">Navigation:</span>
<% if user_logged_in? %>
<%= link_to 'Logout', logout_url %>
<% else %>
<%= link_to "Signup", signup_url %>
| <%= link_to 'Login', login_url %>
<% end %>
| <%= link_to 'Contact Us', 'contact' %>
</div>

</div>

<!-- ##### Main Copy ##### -->

<div id="main-copy">
<% flash.each do |key,value| %>
<div id="flash" class="flash_<%= key %>" >
<span class="message"><%= value %></span>
</div>
<% end -%>

<%= yield :layout %>
</div>

</body>
</html>

And then delete forums.html.erb, replies.html.erb, and topics.html.erb so that only the application.html.erb will show.



Lets go ahead and get some different styling in this. Download this stylesheet and toss it in /public/stylesheets.


Alright! Now we should be able to fire up the server and have a look at what we've got.














Next, open up app/views/sessions/new.html.erb. Uncomment the 'Remember me' section, and then next to the submit button, add:


<%= link_to 'Forgot Password?', forgot_password_url %>

Create /app/views/users/change_password.html.erb:



<% form_for :action => 'change_password' do |f| %>
<fieldset>
<dl>
<dt><label for="old_password" class="block">Old Password</label></dt>
<dd><%= password_field_tag 'old_password', @old_password, :size => 45,
:class => 'text' %></dd>

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

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

</dl>
<%= submit_tag 'Change password' %>
</fieldset>
<%end%>

Create /app/views/users/forgot_password.html.erb



<% form_for :user, :url => {:action => 'forgot_password'} do |form| %>
<fieldset>
<legend>Password Reset Request</legend>
<p>Enter your email address that we have on our file and click send. We will
send you a password reset link email to your email address.</p>
<p>
<label for="user_email" >Email Address:</label><br/>
<%= form.text_field :email, :size => 35, :class => 'text' %>
</p>
<%= submit_tag 'Send' %>
</fieldset>
<% end %>

And create /app/views/users/reset_password.html.erb



<% form_for :user, :url => {:action => "reset_password"} do |form| %>
<fieldset>
<legend>Reset Password</legend>

<p>
<label for="user_password" >Password</label><br/>
<%= form.password_field :password, :size => 45, :class => 'text' %>
</p>
<p>
<label for="user_password_confirmation" >Confirm Password</label><br/>
<%= form.password_field :password_confirmation, :size => 45, :class => 'text' %>
</p>
<%= submit_tag "Reset your password" %>
</fieldset>
<% end %>

We are moving kind of fast here, but most of this stuff should make sense to you, you just need to see it in action. If you have any questions over how anything works, leave a comment and I will get back to you.


Now, in app/controllers/users_controller.rb, lets add in the actions for forgot_password, reset_password, and change_password:



def change_password
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 profile_url(current_user.login)
else
flash[:alert] = "Password not changed"
end

else
flash[:alert] = "New Password mismatch"
@old_password = params[:old_password]
end
else
flash[:alert] = "Old password incorrect"
end
end

#gain email address
def forgot_password
return unless request.post?
if @user = User.find_by_email(params[:user][:email])
@user.forgot_password
@user.save
redirect_back_or_default('/')
flash[:notice] = "A password reset link has been sent to your email address"
else
flash[:alert] = "Could not find a user with that email address"
end
end

#reset password
def reset_password
@user = User.find_by_password_reset_code(params[:id])
return if @user unless params[:user]

if ((params[:user][:password] && params[:user][:password_confirmation]) &&
!params[:user][:password_confirmation].blank?)
self.current_user = @user #for the next two lines to work
current_user.password_confirmation = params[:user][:password_confirmation]
current_user.password = params[:user][:password]
@user.reset_password
flash[:notice] = current_user.save ? "Password reset success." : "Password reset failed."
redirect_back_or_default('/')
else
flash[:alert] = "Password mismatch"
end
end


In config/environments/development.rb you need to add SITE="http://localhost:3000" at the bottom, we will get to this in just a sec.


Now we will take a look at the user_mailer views. They are used to format emails sent out to users. In app/views/user_mailer/activation.html.erb, remove the part where it says "You may now start adding your plugins:". Now lets create app/views/user_mailer/forgot_password.html.erb, and app/views/user_mailer/reset_password.html.erb:



#forgot_password.html.erb

Dear <%= @user.login %>,

We have had a request to reset your password, please visit
<%= @url %>

#reset_password.html.erb

Your have reset the password for your account successfully.

Username: <%= @user.login %>
Password: <%= @user.password %>

Now lets edit app/models/user.rb. Near the bottom, somewhere after the protected keyword, add the follow function:



def make_password_reset_code
self.password_reset_code = Digest::SHA1.hexdigest( Time.now.to_s.split(//).sort_by {rand}.join )
end

And then above protected, add these functions:



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_attributes(: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 recently_activated?
@recent_active
end



And now we need to set up the user_mailer model to use all of this. Change app/models/user_mailer.rb to look like this:



class UserMailer < ActionMailer::Base
def signup_notification(user)
setup_email(user)
@subject += 'Please activate your new account'

@body[:url] = "#{SITE}/activate/#{user.activation_code}"

end

def activation(user)
setup_email(user)
@subject += 'Your account has been activated!'
@body[:url] = "#{SITE}/"
end

def forgot_password(user)
setup_email(user)
@subject += 'You have requested to change your password'
@body[:url] = "#{SITE}/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 %("Rails Forum Admin" ) # Sets the User FROM Name and Email
subject "[Rails Forum] New account information "
body :user => user
sent_on Time.now
end
end

Note where we used the SITE constant that we set in development.rb earlier. Lets stop the server and then restart it since we changed the development.rb. Now, we should be able to go back and sign up.










If you check the console running your server, you will see SQL queries adding the new user to the database, and the email being sent(it won't actually send an email since we are in development)






Go to think link from the console(localhost:3000/activate/whatever) and you will get a message saying 'Signup complete' and you will be logged in. It will also send an email saying that your account has been activated. You can test everything by logging out and logging back in again.


Alright, we are almost done! Next, open up app/models/user_observer.rb and change the after_save function to this:



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

Just more setting up for the mailer.


So, now we have a Signup, a few ways for taking care of forgotten passwords, and email authentication. But how do we block the adding posts and replies to only people logged in? How do we restrict creating forums and deleting posts to only an admin? Open up the file vendor/plugins/restful_authentication/generators/authenticated/templates/authenticated_system.rb and read through some of the comments about the functions:



# Returns true or false if the <%= file_name %> is logged in.
# Preloads @current_<%= file_name %> with the <%= file_name %> model if they're logged in.
def logged_in?
current_<%= file_name %> != :false
end

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

logged_in? checks the value of current_user and returns true if it does not equal :false. current_user tries to login from a session, from basic auth, and from a cookie. If it is unable to log them in, it will return :false and cause the value of logged_in to be false.




# Check if the <%= file_name %> 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 <%= file_name %>
# has the correct rights.
#
# Example:
#
# # only allow nonbobs
# def authorized?
# current_<%= file_name %>.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

We can use before_filter :login_required to stop people from going to /topic/1/posts/2/edit and being able to edit the post. logged_in? can be used in forum and topic's show views, to stop the form for new topics and replies from being shown. This can all be setup however you want, there are plenty of possibilities. What I am going to do is show you how to display different things based on who is logged in, and how to limit certain actions in your controllers. That should give you enough to figure out what you want to do for your specific application. First, open up app/views/forums/show.html.erb:



#find these 3 lines

<h2>New Post</h2>
<%= render :partial => @topic = Topic.new, :locals => { :button_name => 'Create' } %>
<%= link_to 'New Topic', new_forum_topic_path(@forum) %> |

#and put this around them

<% if logged_in? %>
<h2>New Post</h2>
<%= render :partial => @topic = Topic.new, :locals => { :button_name => 'Create' } %>
<%= link_to 'New Topic', new_forum_topic_path(@forum) %> |
<%end>

#and then change this line
<%= link_to 'Edit', edit_forum_path(@forum) %> |

#to this
<% if is_admin? %>
<%= link_to 'Edit', edit_forum_path(@forum) %> |
<% end %>

And now, go to app/controllers/application.rb. In this file we can write functions that will be available to all controllers. Add this to it:



helper_method :is_admin?
def is_admin?
if logged_in? && current_user.login == "admin"
true
else
false
end
end

def admin_required
is_admin? || admin_denied
end

def admin_denied
respond_to do |format|
format.html do
store_location
flash[:notice] = 'You must be an admin to do that.'
redirect_to forums_path
end
end
end

The first line, helper_method :is_admin? will let this function be used in the view. current_user.login is the username for the person logged in. You can change this method however you want to determine whether or not a user is an admin. admin_required and admin_denied will be used in our controllers to restrict access to actions - lets go to our app/controllers/forums_controller.rb and put that in:



#at the very top, right after class ForumsController < ApplicationController, add this line:

before_filter :admin_required, :only => [ :edit, :update, :create, :new, :destroy ]

Now, when you try to access edit, update, create, new, or destroy actions in your controller, if your username is not 'admin', it will go to the show forums page and have a flash saying 'You must be an admin to do that'.


Well, I guess that is it for now. We've added the ability to create users, reset passwords, verify email, and restrict actions to admins. This isn't a completely finished web app. We didn't look into any kind of input validation and we didn't go over all the things we would need to limit to users logged in(create post and create reply), and we didn't limit a lot of things to admins that we should have(such as editing and destroying posts/replies). Hopefully, you've learned enough from this to be able to figure it out yourself, but if you have any trouble feel free to leave a comment. Also, let me know if there are any typos, mistakes, or questions about the tutorial, as well if you have any suggestions. Thanks for reading!
-Ralph



45 comments:

Chouinard said...

I would like to know if you can include some more basics in your tutorials. I think there is not enough information out there about changing the shema once the scaffold is generated. How to add a foreing key and references.

Also I was wondering how we could implement a text editor instead of the textbox in the topics and replies.

Nice tutorial by the way! It got me started on some bigger applications.

rledge21 said...

Thanks for the comment; I covered the references and belongs_to in the first tutorial if you haven't gone through that. Or were you referring to adding those reference columns to something that already exists?

By text editor - do you mean something with spell check? Or copy/paste buttons?

Anonymous said...

All I can say is thanks. Exactly what I needed to start using the :has_many => in my routes.

Your efforts are appreciated.

Kevin

Thomas said...

Hey I am using Instant Rails and don't see the authentication show up in my console window. Is there another way of getting the activation code?

rledge21 said...

Sure is - all you need to do is run the server through the console instead of using the instant rails server..

from the root of your project:

ruby script/server

or if you need to set a port:

ruby script/server -p 4000

Thomas said...

Ha now I know I'm a noob. Thanks. I learned more in this tutorial than I have in the last week.

iHarpreet said...

I am a noob but I have seen and read many tutorials. I have to admit you tutorial is excellent as it is practical and could be used as is in production (of course with some changes).
However, I have one question, I did everything and I signed up a user. The page gave me information that an email has been sent.
Dont I need to set up my email? I did not receive any email so I am guessing I need to configure something to send an email. Please advise. thanks!

iHarpreet said...

Got it: I followed this following tutorial and its GOLD!!

http://www.danielfischer.com/2008/01/09/how-to-use-gmail-as-your-mail-server-for-rails/

rledge21 said...

iHarpreet: Thanks for the link, I'll add it on the Links page. Interesting, I didn't know google let you tag onto their mail server. I guess it makes sense from their point of view though - they do like their information gathering :)

Bill said...

I'm enjoying your tutorials and with some trouble I've managed to get through two of three. I'd like to list some differences I experiencing along this tutorial:

1) The first set of instructions go like this:
cd myforum

ruby script/plugin install http://elitists.textdriven.com/svn/plugins/acts_as_state_machine/trunk
ruby script/plugin install http://svn.techno-weenie.net/projects/plugins/restful_authentication

However I get a messages on my cmd window that say,
Plugin not found: ["http"//elitists.textdriven.com/svn/plugins/acts_as_state_machine/trunk/"] (the message for restful_authentication is similar)

I finally went to the site locations myself and downloaded them file by file into my vendor/plugins directory. then I ran the ruby script/generate authenticated user sessions --include-activation --stateful
command and got an error message saying C:/InstantRails-2.0-win/ruby/lib/ruby/gems/1.8/gems/activesupport-2.0.2/lib/active_record/base.rb:1532:in 'methode_missing': undefined method 'acts_as_states_machine' for Class:0x33f7fe8 (NoMethodeError)

the next line says its from myforum/app/models/user.rb:20


2) Next you say in routes.rb:
#CHANGE THIS LINE
map.resources :users

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

however that line is not present so I just added it.

3) Later you say," Next, lets open up /db/migrate/004_create_users.rb and do this."

but that file does not exist so I created it.

That is when I decided there's enough differences to send a post. Hopefully It is somthing simple. I was very disappointed that the 1st commands to install the plugins did not work.

Regards,
Bill

stuart said...

Seems like the link to the style.css is dead... any way you can re-link it or paste the content to into a comment? Great tutorial, thanks!

Stu

Franco said...

Great tutorial, thanks for that.

I've a question. When you change the routes
map.resources :topics
by
map.resources, :has_many => :replies

I guess that you are building the nested routes here, so, will be the same to do:

map.resources :topics do |topic|
topic.resources :replies
end


There exist any advantages to do one or another?

BTW, the link to the css still is broken.

Thanks.

rledge21 said...

franco,

the loop is the pre-2.0 way of nesting routes, and the method I showed has just become available in 2.0 (I believe).

I don't think there are any advantages/disadvantages between the 2...just that the way I did it is more readable/less code.

I will get those links fixed up tonight, I'm looking into getting a permanent webserver to host things on (and if anyone wants to donate to the cause, shoot me an EMail, I'm a poor college student :) ).

rledge21 said...

Links fixed.

Anonymous said...

Thanks for the tutorial. This helped me get a running version of restful rails.
I couldn't get the activation email once the activation url was entered. If I changed the state to user.pending? then the email arrives with the signup notification, otherwise it doesn't seem to trigger on recently_activated?
Also, the messages appear when revisiting the forums main page. In my case the default was the rails public page and the message doesn't show.
Anyway, thanks again.

Jason said...

This series of tutorials were great and has really helped my along with the newbie application I'm working on.

Just wondering if you could give any tips on how I include the user_id in the topics and replies.

Cheers :)

Patrick Baselier said...

Hi Ralph,

Great tutorial, very easy to implement as well.

I made some small changes to the UserMailer I would like to share.

For setting the url variable I used the routes instead of hard-coding the url:

# Instead of:
# @body[:url] = "#{SITE}/activate/#{user.activation_code}"
# Use:
@body[:url] = activate_url(user.activation_code)

# Instead of:
# @body[:url] = "#{SITE}/"
# Use:
@body[:url] = root_url

# Instead of:
# @body[:url] = "#{SITE}/reset_password/#{user.password_reset_code}"
# Use:
@body[:url] = reset_password_url(user.password_reset_code)

Using the approach you have to set the host in the ActionMailer::Base.default_url_options hash. Herefore I defined a before_filter in the ApplicationController:

# ApplicationController
before_filter :configure_mailer

def configure_mailer
ActionMailer::Base.default_url_options[:host] = defined?(SITE) ? SITE: request.host
ActionMailer::Base.default_url_options[:port] = request.port unless request.port == 80
end


This way you can omit the SITE constant in your [environment].rb. When defined, there's no need to specify the portnumber:
SITE="http://localhost"

greg said...

hi,

warning! Be careful with the reset-password method. If a user exists in the database WITHOUT a password reset code then they will be loaded as the user to reset the password for if the url is accessed without an id/password_reset_code.

in my test case, the admin user didn't have a password_reset_code, so accessing the url with an empty id meant that anyone could reset the admin password - not good.

in users_controller.rb, method reset_password add something along the lines of this;

-----
@user = User.find_by_password_reset_code(params[:id])

# bail out if no user exists for the reset code
if @user.nil?
redirect_to some_other_url
return
end
-----

I also validated the params[:id] a little before that as well.

cheers,

// greg

Mittineague said...

Fantastic! The only things I found a bit confusing were

"Next, open up app/controllers/sessions_controller and copy the line include AuthenticatedSystem to /app/controllers/application.rb. Also, in users_controller, remove the same line. This will make sure that the restful_authentication functions are included on every page."

I was unsure if you meant I should remove the line from the Sessions Controller (only have it in the Application Controller). Having it only in AC worked fine.

The code for app/views/users/change_password.html.erb, forgot_password.html.erb, and reset_password.html.erb, is given twice. The first code example has a mark-up problem

<% end %>
</pre>
<p>Standard stuff there.</p>
<p>Now, open up app/helpers/application_helper.rb and add this code:</p>
<pre>
def user_logged_in?
session[:user_id]
end

And there is a syntax error here

"That should give you enough to figure out what you want to do for your specific application. First, open up app/views/forums/show.html.erb:"

<%= link_to 'New Topic', new_forum_topic_path(@forum) %> |
<%end>

should be

<%= link_to 'New Topic', new_forum_topic_path(@forum) %> |
<% end %>

rledge21 said...

Mittineague, glad you enjoyed the tutorial, thanks for pointing out the typos to me!

"I was unsure if you meant I should remove the line from the Sessions Controller (only have it in the Application Controller). Having it only in AC worked fine."

You are correct, just having it in the application controller is what I was aiming at there.

bayerlin said...

hi, greg

I fined the same problem of your's. i solved with a if check:
if params[:id].blank?
redirect_back_or_default('/')
......
the everything is ok.

Kalimotxo said...

In the latest authenticatedsystem, user_logged_in? was replaced with logged_in? . Fix the application.html.erb code to reflect this.

HopAlong said...

On this line:

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

It uses "member" - I have a model named "member" already - is that going to mess it up? What is that member for?

josh said...

i cant seem to get email working im not sure the problem but im not receiving any emails. could anyone help with this issue?

rledge21 said...

@josh: try installing Postfix...everything should send after that.
Until you have something setup to actually send the emails, the only place you can read them is in the console.

Anonymous said...

thank you for great tutorial i works great on my local machine..
I have a very simple question since i'm just a newbie on RoR.If i'm ready to deploy my project.What do i need to change on your code since the tutorial design for development.

Chris Doten said...

Fantastic tutorial. Learned a tremendous amount about auth and keeping state.

Hasinur Rahman said...

Really good post. Thanks for this great posts.
But I am getting the following error:

undefined method `acts_as_state_machine' for #Class:0x46edaf8

mkoppel2040 said...

Hasinur,

If you downloaded the latest technoweenie RESTFUL Authentication there is a README.textile that contains a link to the plugin you need.

I'm having a problem with the create method in my user controller. I keep getting the following error.

NoMethodError in UsersController#create
undefined method `name' for #User

I'm on 2.3.2. fyi. thanks!

Sandip Gangakhedkar said...

Hi..I've been following your tutorial closely and have made just one change: removed the login field and added email and password validation. Everything works fine until I copy and paste the activation link from the console:

We couldn't find a user with that activation code -- check your email? Or maybe you've already activated -- try signing in.

I don't know whats going on since I'm pretty sure I cleared out all the login related code. Your thoughts on this will be greatly appreciated.

Wonder if we could communicate on the Rails channel on freenode..will be much more efficient. I'm 'bobsaccamano' on Freenode. Thanks.

knm said...

Thankyou for the tutorial.It gave a good start.I would appreciate for your good work.

Anonymous said...

Pay attention, may be this is not an issue for everybody, but if you should have problems, that the sent activation_code does match with that in the database stored, reload your user object before sending its data through email something like:


class UserObserver < ActiveRecord::Observer
def after_create(user)
user.reload
UserMailer.deliver_signup_notification(user)
end
def after_save(user)
user.reload
UserMailer.deliver_activation(user) if user.recently_activated?
end
end


http://github.com/technoweenie/restful-authentication/tree/master

Mr. WHo said...

Help!! I'm a newbie, I did everything in this tutorial,
It worked until I don't know if activation works.. how to test it?
how to see it thru sql queries?

also, I couldn't login or logout. Is there any logout optionn?

thank you

Anonymous said...

wonderful tutorial!!!!
I have installed postfix... than also i m not able to send mail... It gives me message like ur mail has been sent bt i m not able to receive in my mail account.... Plz help me out of this...

Anonymous said...

I too had trouble with the activation code being different in the database and in the activation email. The fix suggested by Anonymous on May 15, 2009 worked.

Thanks

class UserObserver < ActiveRecord::Observer
def after_create(user)
user.reload
UserMailer.deliver_signup_notification(user)
end
def after_save(user)
user.reload
UserMailer.deliver_activation(user) if user.recently_activated?
end
end

Anonymous said...

There is a mistake in the Tutorial.
___

Search for:

#and put this around them

and go to:
< % end >

__

Should be < % end % >

___

The end-tag was wrong!!!

__

Nice tutorial!

Anonymous said...

Who knows where to download XRumer 5.0 Palladium?
Help, please. All recommend this program to effectively advertise on the Internet, this is the best program!

Anonymous said...

Hi. I've finded a lots of tutorials regarding restful_authentication, but can't receive any result using any of them (

Using this i've received error "undefined method `logged_in?' for #ActionView::Base:0xb7204574". I've finded that this problem catch if string "include AuthenticatedSystem" not in application.rb. I have this string in my application.rb. I'm use rails 2.3.5

May be anyone else have any ideas how to fix this?

Anonymous said...

In previous comment i describe problem, but solution was very simple: now application.rb should be renamed to application_controller.rb

texmex said...

Nice tutorial. Thanks.

Email for signup ist sent (can see it in the console).
Copying the link to visit into browser does not activate the new user, and console don't show email sent. Do not get message 'Your account has been activated'.

Noticed in user.rb following possible duplication:

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

Would appreciate any hint on that.

Thank you

David said...

Do you have any unit tests for the added methods (change/reset/forgot_password)?

tom said...

hi,

the activation code in the email is a diffrent one then the one in the database...
am i the only one who is having that?
thx

Alex Agranov said...

The activation code sent in signup notification email is different from the one stored with the new user instance in the DB because User.make_activation_code is being called twice by the AASM layer as currently leveraged by restful_authentication.

'make_activation_code' has been declared to be fired on a User instance when it moves from state :pending.

The first time it is being called due to the manual state change in UserController::create.

/Users/agranov/work/sb/app/models/user.rb:82:in `make_activation_code'
/Users/agranov/work/sb/vendor/plugins/acts_as_state_machine/lib/acts_as_state_machine.rb:162:in `call'
/Users/agranov/work/sb/vendor/plugins/acts_as_state_machine/lib/acts_as_state_machine.rb:162:in `run_transition_action'
/Users/agranov/work/sb/vendor/plugins/acts_as_state_machine/lib/acts_as_state_machine.rb:23:in `entering'
/Users/agranov/work/sb/vendor/plugins/acts_as_state_machine/lib/acts_as_state_machine.rb:59:in `perform'
/Users/agranov/work/sb/vendor/plugins/acts_as_state_machine/lib/acts_as_state_machine.rb:94:in `block in fire'
/Users/agranov/work/sb/vendor/plugins/acts_as_state_machine/lib/acts_as_state_machine.rb:93:in `each'
/Users/agranov/work/sb/vendor/plugins/acts_as_state_machine/lib/acts_as_state_machine.rb:93:in `fire'
/Users/agranov/work/sb/vendor/plugins/acts_as_state_machine/lib/acts_as_state_machine.rb:201:in `block in event'
/Users/agranov/work/sb/app/controllers/users_controller.rb:33:in `create'

It is called a second time as a result of callback processing:

/Users/agranov/work/sb/app/models/user.rb:82:in `make_activation_code'
/Users/agranov/work/sb/vendor/plugins/acts_as_state_machine/lib/acts_as_state_machine.rb:162:in `call'
/Users/agranov/work/sb/vendor/plugins/acts_as_state_machine/lib/acts_as_state_machine.rb:162:in `run_transition_action'
/Users/agranov/work/sb/vendor/plugins/acts_as_state_machine/lib/acts_as_state_machine.rb:23:in `entering'
/Users/agranov/work/sb/vendor/plugins/acts_as_state_machine/lib/acts_as_state_machine.rb:140:in `run_initial_state_actions'
/usr/local/lib/ruby/gems/1.9.1/gems/activesupport-2.3.8/lib/active_support/callbacks.rb:178:in `evaluate_method'
/usr/local/lib/ruby/gems/1.9.1/gems/activesupport-2.3.8/lib/active_support/callbacks.rb:166:in `call'
/usr/local/lib/ruby/gems/1.9.1/gems/activesupport
[snip...]
/usr/local/lib/ruby/gems/1.9.1/gems/activerecord-2.3.8/lib/active_record/transactions.rb:228:in `with_transaction_returning_status'
/usr/local/lib/ruby/gems/1.9.1/gems/activerecord-2.3.8/lib/active_record/transactions.rb:196:in `block in save_with_transactions'
/usr/local/lib/ruby/gems/1.9.1/gems/activerecord-2.3.8/lib/active_record/transactions.rb:208:in `rollback_active_record_state!'
/usr/local/lib/ruby/gems/1.9.1/gems/activerecord-2.3.8/lib/active_record/transactions.rb:196:in `save_with_transactions'
/usr/local/lib/ruby/gems/1.9.1/gems/activerecord-2.3.8/lib/active_record/base.rb:2657:in `update_attribute'
/Users/agranov/work/sb/vendor/plugins/acts_as_state_machine/lib/acts_as_state_machine.rb:61:in `perform'
/Users/agranov/work/sb/vendor/plugins/acts_as_state_machine/lib/acts_as_state_machine.rb:94:in `block in fire'
/Users/agranov/work/sb/vendor/plugins/acts_as_state_machine/lib/acts_as_state_machine.rb:93:in `each'
/Users/agranov/work/sb/vendor/plugins/acts_as_state_machine/lib/acts_as_state_machine.rb:93:in `fire'
/Users/agranov/work/sb/vendor/plugins/acts_as_state_machine/lib/acts_as_state_machine.rb:201:in `block in event'
/Users/agranov/work/sb/app/controllers/users_controller.rb:33:in `create'

Rather than reloading the User instance, a quicker fix is simply to only allow initialization of activation_code once, as in:

def make_activation_code
self.deleted_at = nil
self.activation_code ||= self.class.make_token
end

Sorry for the massive post. ;-)

-s

PursuitOfHappiness said...

Hi, Thanks for tutorial, In the tutorial there is provision of two users only, I am trying to add one more user i.e New role like admin,super user, user but i don't know how to add another role in this could please help me out on htis

russian830 said...

hey great tutorial, using it to get through a software engineering project in school. Professor says who know Rails, no hands go up, ok thats what were using. any case, a lot of the code is not be antiquated, any chance of redoing another tutorial with the undated code for the newer version of rails? I completed tutorial 1 today and can give you the code, took forever to figure out the paths but once i got it I can explain to anyone.

Dave