Every repository with this icon (
Every repository with this icon (
User Activation
- Be careful !! *
User Activation is not secured. You need to add the following line to your user model : attr_protected :activated_at
User activation is an unfortunate, but usually necessary feature of modern web applications. One way to enable this is by creating two extra fields in your user model:
add_column :users, :activation_code, :string, :limit => 40
add_column :users, :activated_at, :datetime
Also, you may want to populate the :activated_at column for existing data. The User.authenticate method listed below will not return the user until :activated_at has a value.
- using ActiveRecord
User.update_all [‘activated_at = ?’, Time.now]
- for MySql
execute ‘update users set activated_at = NOW()’
Now that the columns are in place, you’ll need to update the user model to support this. You’ll want to generate some unique activation code when the user is created, and make sure that #authenticate does not authenticate any users that haven’t been activated.
class User < ActiveRecord::Base before_create :make_activation_code- Authenticates a user by their login name and unencrypted password. Returns the user or nil.
def self.authenticate(login, password) - hide records with a nil activated_at
u = find :first, :conditions => [‘login = ? and activated_at IS NOT NULL’, login]
u && u.authenticated?(password) ? u : nil
end
- Activates the user in the database.
def activate
@activated = true
update_attributes(:activated_at => Time.now.utc, :activation_code => nil)
end
- Returns true if the user has just been activated.
def recently_activated?
@activated
end
- If you’re going to use activation, uncomment this too
def make_activation_code
self.activation_code = Digest::SHA1.hexdigest( Time.now.to_s.split(//).sort_by {rand}.join )
end
end
Before running the existing unit tests, update the users.yml file under test/fixtures. There are two lines in the file that need uncommenting to populate the new database columns.
Now, you’ll need a controller action to activate a user by a given activation code. Here’s a functional test for it:
def test_should_activate_user
assert_nil User.authenticate(‘arthur’, ‘test’)
get :activate, :id => users(:arthur).activation_code
assert_equal users(:arthur), User.authenticate(‘arthur’, ‘test’)
end
Here’s what the controller action could look like:
def activate
@user = User.find_by_activation_code(params[:id])
if @user and @user.activate
self.current_user = @user
redirect_back_or_default(:controller => ‘/account’, :action => ‘index’)
flash[:notice] = “Your account has been activated.”
end
end
You also may have to add a dummy test in the notifier or your tests will fail. Here is what I did:
def test_dummy_test
#do nothing
end
Additional Greg Houston:
After using the Activation code above, I noted a couple of tests needed adding. Also, I thought it would help to have an activation page that lets a user type in their activation code. This provides a good target for failed activation requests.
In the code below, I use flash[:error] instead of flash[:notice]. My template will display the errors and notices differently. If you only use flash[:notice] it is easy to change the code.
The tests first… :)
class AccountControllerTest < Test::Unit::TestCase
def test_should_not_activate_nil get :activate, :activation_code => nil assert_activate_error end def test_should_not_activate_bad get :activate, :activation_code => ‘foobar’ assert flash.has_key?(:error), “Flash should contain error message.” assert_activate_error end def assert_activate_error assert_response :success assert_template “account/activate” endend
The account/activate.rhtml…
<%= start_form_tag :action => “activate” >
<
<%= submit_tag ‘Activate’ %>
<%= end_form_tag %>
And finally the updated activate method that passes the tests…
def activate if params[:activation_code] @user = User.find_by_activation_code(params[:activation_code]) if @user and @user.activate self.current_user = @user redirect_back_or_default(:controller => ‘/account’, :action => ‘index’) flash[:notice] = “Your account has been activated.” else flash[:error] = “Unable to activate the account. Did you provide the correct information?” end else flash.clear end endMore from Keegan Quinn:
I’ve made use of the improvements suggested by Greg Houston, and found a small problem. After replacing the activate method with Greg’s version, the original test_should_activate_user no longer works. To get things working again, I replaced the :id symbol in the get method call with :activation_code.
It seems to me that Greg’s improvements and this note should all be integrated back into the text at the top. I volunteer to do that if nobody objects or beats me to it.
Bugfix from Lewis Hoffman:
I changed the code above in make_activation_code() from split(’//’) to split(//). split(’//’) always returns the same string, so we ran into a bug where multiple users created the same second had the same activation code!!
Bugfix
The password in my fixtures for arthur was set to ‘test’ not ‘arthur,’ so I changed the example above.
Suggestion from Ilya Grigorik:
If you include user activation code in your implementation, alter the signup method in your controller to disable auto-login on registration. Comment out self.current_user = @user and the user will not be automatically logged in.
Why? Well, the unfortunate point of user verification process through email is to put up some barriers against online bots which will register and wreck havoc on your site, and if you auto-magically log them in after sign-up, what’s the point? You only annoy the users and forego the point of the activation itself. Instead, take your new user to a welcome page which will explain how to activate their account. Ex: comment out redirect_back_to_failed and instead add render :action => ‘welcome’. Add a welcome.rhtml file to your views directory (don’t need to add anything to the controller) and you’re done.
Suggestion from Tom Henely:
I agree with Greg. An authentication page is very useful, but I wanted to maintain the ability to have users authenticate with a direct link. So I merged the original code with Gregs.
def activate
if params[:activation_code]
@user = User.find_by_activation_code(params[:activation_code])
if @user and @user.activate
self.current_user = @user
redirect_back_or_default(:controller => ‘/account’, :action => ‘index’)
flash[:notice] = “Your account has been activated.”
else
flash[:error] = “Unable to activate the account. Did you provide the correct information?”
end
elsif params[:id]
@user = User.find_by_activation_code(params[:id])
if @user and @user.activate
self.current_user = @user
redirect_back_or_default(:controll => ‘/account’, :action => ‘index’)
flash[:notice] = “Your account has been activated”
end
else
flash.clear
end
end
This checks for both the post from the page and the get that may be passed to it through a direct link.
Suggestion from Labrat:
This is basically a concise re-implementation of the method above. Also doesn’t automatically log in activated users.
def activate flash.clear return if params[:id] nil and params[:activation_code] nil activator = params[:id] || params[:activation_code] @user = User.find_by_activation_code(activator) if @user and @user.activate redirect_back_or_default(:controller => ’/account’, :action => ‘login’) flash[:notice] = “Your account has been activated. Please login.” else flash[:notice] = “Unable to activate the account. Please check or enter manually.” end endBugfix from Barry Hess:
There is a pretty drastic security bug in the activation action in the controller code (original version). The bug’s existence assumes you are logging people in upon activation. In the “find_by_activation_code” section you need to make sure the id parameter exists or else someone coming in on “http://www.yoursite.com/account/activate” will be given full access to the first user’s account it finds with a NULL activation code.
Just add an “if params[:id]” on the back of the first line in the controller to fix the bug.
Not a bug, but it may also be useful to stick an else on that if clause and redirect failed activations with a flash[:notice]. This can help resolve confusion when someone forgetfully clicks an activation link a second time. My new app has 8 users and this error case has already occurred. :)
def activate @user = User.find_by_activation_code(params[:id]) if params[:id] if @user and @user.activate self.current_user = @user redirect_back_or_default(:controller => ‘/profile’, :action => ‘index’) flash[:notice] = “Your account has been activated.” else redirect_back_or_default(:controller => ‘home’, :action => ‘index’) flash[:notice] = “It looks like you’re trying to activate an account. Perhaps have already activated this account?” end endhttp://toolmantim.com/article/2007/1/31/skinny_user_activation
Tip from Todd:
There is no arthur in the user.yml file. I altered my user.yml file and changed aaron to be arthur. (Alternatively you could rename the test cases on this page.)
Bugfix from Jub:
If you use “attr_protected :activated_at” as suggest at the top of this page, you have to change your User.activate method, because update_attributes won’t work.
I changed it for this :
Bugfix from bublik:







