thoughtbot-clearance 0.7.0 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.textile +8 -0
- data/README.textile +34 -34
- data/Rakefile +1 -1
- data/app/controllers/clearance/confirmations_controller.rb +4 -2
- data/app/controllers/clearance/passwords_controller.rb +6 -3
- data/app/controllers/clearance/sessions_controller.rb +1 -2
- data/app/views/clearance_mailer/change_password.html.erb +3 -1
- data/app/views/clearance_mailer/confirmation.html.erb +4 -1
- data/app/views/passwords/edit.html.erb +1 -1
- data/app/views/sessions/new.html.erb +2 -6
- data/generators/clearance/templates/migrations/create_users.rb +5 -4
- data/generators/clearance/templates/migrations/update_users.rb +11 -10
- data/generators/clearance_features/clearance_features_generator.rb +1 -1
- data/generators/clearance_features/templates/features/sign_in.feature +0 -7
- data/generators/clearance_features/templates/features/sign_out.feature +0 -8
- data/generators/clearance_features/templates/features/step_definitions/clearance_steps.rb +14 -8
- data/generators/clearance_views/templates/formtastic/passwords/edit.html.erb +1 -1
- data/generators/clearance_views/templates/formtastic/sessions/new.html.erb +0 -1
- data/lib/clearance/authentication.rb +56 -39
- data/lib/clearance/user.rb +107 -25
- data/shoulda_macros/clearance.rb +3 -3
- metadata +3 -5
data/CHANGELOG.textile
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
+
h2. 0.8.0 (08/31/2009)
|
2
|
+
|
3
|
+
* Always remember me. Replaced session-and-remember-me authentication with
|
4
|
+
always using a cookie with a long timeout. (Dan Croak)
|
5
|
+
* Documented Clearance::Authentication with YARD. (Dan Croak)
|
6
|
+
* Documented Clearance::User with YARD. (Dan Croak)
|
7
|
+
|
1
8
|
h2. 0.7.0 (08/04/2009)
|
2
9
|
|
3
10
|
* Redirect signed in user who clicks confirmation link again. (Dan Croak)
|
@@ -57,6 +64,7 @@ h2. 0.6.2 (04/22/2009)
|
|
57
64
|
* World(NavigationHelpers) Cucumber 3.0 style. (Shay Arnett & Mark Cornick)
|
58
65
|
|
59
66
|
h2. 0.6.1 (04/21/2009)
|
67
|
+
|
60
68
|
* Scope operators are necessary to keep Rails happy. Reverting the original
|
61
69
|
revert so they're back in the library now for constants referenced inside of
|
62
70
|
the gem. (Nick Quaranto)
|
data/README.textile
CHANGED
@@ -4,9 +4,9 @@ Rails authentication with email & password.
|
|
4
4
|
|
5
5
|
"We have clearance, Clarence.":http://www.youtube.com/v/mNRXJEE3Nz8
|
6
6
|
|
7
|
-
h2.
|
7
|
+
h2. Suspenders
|
8
8
|
|
9
|
-
|
9
|
+
Clearance is included in "Suspenders":http://github.com/thoughtbot/suspenders, which thoughtbot uses on all of our apps. We highly recommend you try it. Suspenders is the "King Gem" in our ecosystem, representating what we think the current state-of-the-art is in Rails development.
|
10
10
|
|
11
11
|
h2. Installation
|
12
12
|
|
@@ -15,10 +15,10 @@ Clearance is a Rails engine. It works with versions of Rails greater than 2.3.
|
|
15
15
|
In config/environment.rb:
|
16
16
|
|
17
17
|
<pre>
|
18
|
-
config.gem "thoughtbot-clearance",
|
19
|
-
:lib => 'clearance',
|
20
|
-
:source => 'http://gems.github.com',
|
21
|
-
:version => '0.
|
18
|
+
config.gem "thoughtbot-clearance",
|
19
|
+
:lib => 'clearance',
|
20
|
+
:source => 'http://gems.github.com',
|
21
|
+
:version => '0.7.0'
|
22
22
|
</pre>
|
23
23
|
|
24
24
|
Vendor the gem:
|
@@ -30,7 +30,9 @@ rake gems:unpack
|
|
30
30
|
|
31
31
|
Make sure the development database exists and run the generator:
|
32
32
|
|
33
|
-
|
33
|
+
<pre>
|
34
|
+
script/generate clearance
|
35
|
+
</pre>
|
34
36
|
|
35
37
|
A number of files will be created and instructions will be printed.
|
36
38
|
|
@@ -38,51 +40,37 @@ You may already have some of these files. Don't worry. You'll be asked if you wa
|
|
38
40
|
|
39
41
|
Run the migration:
|
40
42
|
|
41
|
-
|
43
|
+
<pre>
|
44
|
+
rake db:migrate
|
45
|
+
</pre>
|
46
|
+
|
47
|
+
h2. If you aren't on Suspenders, you aren't done
|
42
48
|
|
43
49
|
Define a HOST constant in your environment files.
|
44
50
|
In config/environments/test.rb and config/environments/development.rb it can be:
|
45
51
|
|
46
|
-
|
52
|
+
<pre>
|
53
|
+
HOST = "localhost"
|
54
|
+
</pre>
|
47
55
|
|
48
56
|
In production.rb it must be the actual host your application is deployed to.
|
49
57
|
The constant is used by mailers to generate URLs in emails.
|
50
58
|
|
51
59
|
In config/environment.rb:
|
52
60
|
|
53
|
-
@DO_NOT_REPLY = "donotreply@example.com"@
|
54
|
-
|
55
|
-
Define root_url to *something* in your config/routes.rb:
|
56
|
-
|
57
|
-
@map.root :controller => 'home'@
|
58
|
-
|
59
|
-
h2. Cucumber Features
|
60
|
-
|
61
|
-
As your app evolves, you want to know that authentication still works. Clearance's opinion is that you should test its integration with your app using "Cucumber":http://cukes.info/.
|
62
|
-
|
63
|
-
In config/environments/test.rb:
|
64
|
-
|
65
61
|
<pre>
|
66
|
-
|
67
|
-
:version => '= 0.4.4'
|
68
|
-
config.gem 'cucumber',
|
69
|
-
:version => '= 0.3.0'
|
70
|
-
config.gem 'thoughtbot-factory_girl',
|
71
|
-
:lib => 'factory_girl',
|
72
|
-
:source => "http://gems.github.com",
|
73
|
-
:version => '1.2.1'
|
62
|
+
DO_NOT_REPLY = "donotreply@example.com"
|
74
63
|
</pre>
|
75
64
|
|
76
|
-
|
65
|
+
Define root_url to *something* in your config/routes.rb:
|
77
66
|
|
78
67
|
<pre>
|
79
|
-
|
80
|
-
rake gems:unpack RAILS_ENV=test
|
68
|
+
map.root :controller => 'home'
|
81
69
|
</pre>
|
82
70
|
|
83
|
-
|
71
|
+
h2. Cucumber Features
|
84
72
|
|
85
|
-
|
73
|
+
As your app evolves, you want to know that authentication still works. thoughtbot's opinion is that you should test its integration with your app using "Cucumber":http://cukes.info/.
|
86
74
|
|
87
75
|
Run the Cucumber generator (if you haven't already) and Clearance's feature generator:
|
88
76
|
|
@@ -107,6 +95,18 @@ def path_to(page_name)
|
|
107
95
|
end
|
108
96
|
</pre>
|
109
97
|
|
98
|
+
h2. Formtastic views
|
99
|
+
|
100
|
+
We have begun standardizing our forms using "Formtastic":http://github.com/justinfrench/formtastic. We highly recommend trying it. It will make your Rails view life more interesting.
|
101
|
+
|
102
|
+
Clearance has another generator to generate Formastic views:
|
103
|
+
|
104
|
+
<pre>
|
105
|
+
script/generate clearance_views
|
106
|
+
</pre>
|
107
|
+
|
108
|
+
Its implementation is designed so that other view styles can be generated if the community wants it. However, we haven't needed them so you'll have to write the patch and send it back if you want other styles (such as Haml).
|
109
|
+
|
110
110
|
h2. Authors
|
111
111
|
|
112
112
|
Clearance was extracted out of "Hoptoad":http://hoptoadapp.com. We merged the authentication code from two of thoughtbot's clients' Rails apps and have since used it each time we need authentication. The following people have improved the library. Thank you!
|
data/Rakefile
CHANGED
@@ -80,7 +80,7 @@ task :default => ['test:basic', 'test:features',
|
|
80
80
|
|
81
81
|
gem_spec = Gem::Specification.new do |gem_spec|
|
82
82
|
gem_spec.name = "clearance"
|
83
|
-
gem_spec.version = "0.
|
83
|
+
gem_spec.version = "0.8.0"
|
84
84
|
gem_spec.summary = "Rails authentication with email & password."
|
85
85
|
gem_spec.email = "support@thoughtbot.com"
|
86
86
|
gem_spec.homepage = "http://github.com/thoughtbot/clearance"
|
@@ -13,7 +13,8 @@ class Clearance::ConfirmationsController < ApplicationController
|
|
13
13
|
end
|
14
14
|
|
15
15
|
def create
|
16
|
-
@user = ::User.
|
16
|
+
@user = ::User.find_by_id_and_confirmation_token(
|
17
|
+
params[:user_id], params[:token])
|
17
18
|
@user.confirm_email!
|
18
19
|
|
19
20
|
sign_in(@user)
|
@@ -46,7 +47,8 @@ class Clearance::ConfirmationsController < ApplicationController
|
|
46
47
|
end
|
47
48
|
|
48
49
|
def forbid_non_existent_user
|
49
|
-
unless ::User.
|
50
|
+
unless ::User.find_by_id_and_confirmation_token(
|
51
|
+
params[:user_id], params[:token])
|
50
52
|
raise ActionController::Forbidden, "non-existent user"
|
51
53
|
end
|
52
54
|
end
|
@@ -22,12 +22,14 @@ class Clearance::PasswordsController < ApplicationController
|
|
22
22
|
end
|
23
23
|
|
24
24
|
def edit
|
25
|
-
@user = ::User.
|
25
|
+
@user = ::User.find_by_id_and_confirmation_token(
|
26
|
+
params[:user_id], params[:token])
|
26
27
|
render :template => 'passwords/edit'
|
27
28
|
end
|
28
29
|
|
29
30
|
def update
|
30
|
-
@user = ::User.
|
31
|
+
@user = ::User.find_by_id_and_confirmation_token(
|
32
|
+
params[:user_id], params[:token])
|
31
33
|
|
32
34
|
if @user.update_password(params[:user][:password],
|
33
35
|
params[:user][:password_confirmation])
|
@@ -49,7 +51,8 @@ class Clearance::PasswordsController < ApplicationController
|
|
49
51
|
end
|
50
52
|
|
51
53
|
def forbid_non_existent_user
|
52
|
-
unless ::User.
|
54
|
+
unless ::User.find_by_id_and_confirmation_token(
|
55
|
+
params[:user_id], params[:token])
|
53
56
|
raise ActionController::Forbidden, "non-existent user"
|
54
57
|
end
|
55
58
|
end
|
@@ -17,7 +17,6 @@ class Clearance::SessionsController < ApplicationController
|
|
17
17
|
else
|
18
18
|
if @user.email_confirmed?
|
19
19
|
sign_in(@user)
|
20
|
-
remember(@user) if remember?
|
21
20
|
flash_success_after_create
|
22
21
|
redirect_back_or(url_after_create)
|
23
22
|
else
|
@@ -29,7 +28,7 @@ class Clearance::SessionsController < ApplicationController
|
|
29
28
|
end
|
30
29
|
|
31
30
|
def destroy
|
32
|
-
|
31
|
+
sign_out(current_user)
|
33
32
|
flash_success_after_destroy
|
34
33
|
redirect_to(url_after_destroy)
|
35
34
|
end
|
@@ -2,6 +2,8 @@ Someone, hopefully you, has requested that we send you a link to change your pas
|
|
2
2
|
|
3
3
|
Here's the link:
|
4
4
|
|
5
|
-
<%= edit_user_password_url(@user,
|
5
|
+
<%= edit_user_password_url(@user,
|
6
|
+
:token => @user.confirmation_token,
|
7
|
+
:escape => false) %>
|
6
8
|
|
7
9
|
If you didn't request this, ignore this email. Don't worry. Your password hasn't been changed.
|
@@ -7,7 +7,7 @@
|
|
7
7
|
<%= error_messages_for :user %>
|
8
8
|
|
9
9
|
<% form_for(:user,
|
10
|
-
:url => user_password_path(@user, :token
|
10
|
+
:url => user_password_path(@user, :token => @user.confirmation_token),
|
11
11
|
:html => { :method => :put }) do |form| %>
|
12
12
|
<div class="password_field">
|
13
13
|
<%= form.label :password, "Choose password" %>
|
@@ -3,16 +3,12 @@
|
|
3
3
|
<% form_for :session, :url => session_path do |form| %>
|
4
4
|
<div class="text_field">
|
5
5
|
<%= form.label :email %>
|
6
|
-
<%= form.text_field :email %>
|
6
|
+
<%= form.text_field :email %>
|
7
7
|
</div>
|
8
8
|
<div class="text_field">
|
9
9
|
<%= form.label :password %>
|
10
10
|
<%= form.password_field :password %>
|
11
11
|
</div>
|
12
|
-
<div class="text_field">
|
13
|
-
<%= form.check_box :remember_me %>
|
14
|
-
<%= form.label :remember_me %>
|
15
|
-
</div>
|
16
12
|
<div class="submit_field">
|
17
13
|
<%= form.submit "Sign in", :disable_with => "Please wait..." %>
|
18
14
|
</div>
|
@@ -25,4 +21,4 @@
|
|
25
21
|
<li>
|
26
22
|
<%= link_to "Forgot password?", new_password_path %>
|
27
23
|
</li>
|
28
|
-
</ul>
|
24
|
+
</ul>
|
@@ -4,15 +4,16 @@ class ClearanceCreateUsers < ActiveRecord::Migration
|
|
4
4
|
t.string :email
|
5
5
|
t.string :encrypted_password, :limit => 128
|
6
6
|
t.string :salt, :limit => 128
|
7
|
-
t.string :
|
8
|
-
t.
|
7
|
+
t.string :confirmation_token, :limit => 128
|
8
|
+
t.string :remember_token, :limit => 128
|
9
|
+
t.datetime :remember_token_expires_at
|
9
10
|
t.boolean :email_confirmed, :default => false, :null => false
|
10
11
|
t.timestamps
|
11
12
|
end
|
12
13
|
|
13
|
-
add_index :users, [:id, :
|
14
|
+
add_index :users, [:id, :confirmation_token]
|
14
15
|
add_index :users, :email
|
15
|
-
add_index :users, :
|
16
|
+
add_index :users, :remember_token
|
16
17
|
end
|
17
18
|
|
18
19
|
def self.down
|
@@ -1,36 +1,37 @@
|
|
1
1
|
class ClearanceUpdateUsers < ActiveRecord::Migration
|
2
2
|
def self.up
|
3
|
-
<%
|
3
|
+
<%
|
4
4
|
existing_columns = ActiveRecord::Base.connection.columns(:users).collect { |each| each.name }
|
5
5
|
columns = [
|
6
6
|
[:email, 't.string :email'],
|
7
7
|
[:encrypted_password, 't.string :encrypted_password, :limit => 128'],
|
8
|
-
[:salt,
|
9
|
-
[:
|
10
|
-
[:
|
11
|
-
[:
|
12
|
-
|
8
|
+
[:salt, 't.string :salt, :limit => 128'],
|
9
|
+
[:confirmation_token, 't.string :confirmation_token, :limit => 128'],
|
10
|
+
[:remember_token, 't.string :remember_token, :limit => 128'],
|
11
|
+
[:remember_token_expires_at, 't.datetime :remember_token_expires_at'],
|
12
|
+
[:email_confirmed, 't.boolean :email_confirmed, :default => false, :null => false']
|
13
|
+
].delete_if {|c| existing_columns.include?(c.first.to_s)}
|
13
14
|
-%>
|
14
15
|
change_table(:users) do |t|
|
15
16
|
<% columns.each do |c| -%>
|
16
17
|
<%= c.last %>
|
17
18
|
<% end -%>
|
18
19
|
end
|
19
|
-
|
20
|
+
|
20
21
|
<%
|
21
22
|
existing_indexes = ActiveRecord::Base.connection.indexes(:users)
|
22
23
|
index_names = existing_indexes.collect { |each| each.name }
|
23
24
|
new_indexes = [
|
24
|
-
[:
|
25
|
+
[:index_users_on_id_and_confirmation_token, 'add_index :users, [:id, :confirmation_token]'],
|
25
26
|
[:index_users_on_email, 'add_index :users, :email'],
|
26
|
-
[:
|
27
|
+
[:index_users_on_remember_token, 'add_index :users, :remember_token']
|
27
28
|
].delete_if { |each| index_names.include?(each.first.to_s) }
|
28
29
|
-%>
|
29
30
|
<% new_indexes.each do |each| -%>
|
30
31
|
<%= each.last %>
|
31
32
|
<% end -%>
|
32
33
|
end
|
33
|
-
|
34
|
+
|
34
35
|
def self.down
|
35
36
|
change_table(:users) do |t|
|
36
37
|
<% unless columns.empty? -%>
|
@@ -9,7 +9,7 @@ class ClearanceFeaturesGenerator < Rails::Generator::Base
|
|
9
9
|
"features/step_definitions/factory_girl_steps.rb",
|
10
10
|
"features/support/paths.rb",
|
11
11
|
"features/sign_in.feature",
|
12
|
-
"features/sign_out.feature",
|
12
|
+
"features/sign_out.feature",
|
13
13
|
"features/sign_up.feature",
|
14
14
|
"features/password_reset.feature"].each do |file|
|
15
15
|
m.file file, file
|
@@ -30,13 +30,6 @@ Feature: Sign in
|
|
30
30
|
And I sign in as "email@person.com/password"
|
31
31
|
Then I should see "Signed in"
|
32
32
|
And I should be signed in
|
33
|
-
|
34
|
-
Scenario: User signs in and checks "remember me"
|
35
|
-
Given I am signed up and confirmed as "email@person.com/password"
|
36
|
-
When I go to the sign in page
|
37
|
-
And I sign in with "remember me" as "email@person.com/password"
|
38
|
-
Then I should see "Signed in"
|
39
|
-
And I should be signed in
|
40
33
|
When I return next time
|
41
34
|
Then I should be signed in
|
42
35
|
|
@@ -10,14 +10,6 @@ Feature: Sign out
|
|
10
10
|
And I sign out
|
11
11
|
Then I should see "Signed out"
|
12
12
|
And I should be signed out
|
13
|
-
|
14
|
-
Scenario: User who was remembered signs out
|
15
|
-
Given I am signed up and confirmed as "email@person.com/password"
|
16
|
-
When I sign in with "remember me" as "email@person.com/password"
|
17
|
-
Then I should be signed in
|
18
|
-
And I sign out
|
19
|
-
Then I should see "Signed out"
|
20
|
-
And I should be signed out
|
21
13
|
When I return next time
|
22
14
|
Then I should be signed out
|
23
15
|
|
@@ -39,6 +39,11 @@ When /^session is cleared$/ do
|
|
39
39
|
controller.instance_variable_set(:@_current_user, nil)
|
40
40
|
end
|
41
41
|
|
42
|
+
Given /^I have signed in with "(.*)\/(.*)"$/ do |email, password|
|
43
|
+
Given %{I am signed up and confirmed as "#{email}/#{password}"}
|
44
|
+
And %{I sign in as "#{email}/#{password}"}
|
45
|
+
end
|
46
|
+
|
42
47
|
# Emails
|
43
48
|
|
44
49
|
Then /^a confirmation message should be sent to "(.*)"$/ do |email|
|
@@ -46,13 +51,14 @@ Then /^a confirmation message should be sent to "(.*)"$/ do |email|
|
|
46
51
|
sent = ActionMailer::Base.deliveries.first
|
47
52
|
assert_equal [user.email], sent.to
|
48
53
|
assert_match /confirm/i, sent.subject
|
49
|
-
assert !user.
|
50
|
-
assert_match /#{user.
|
54
|
+
assert !user.confirmation_token.blank?
|
55
|
+
assert_match /#{user.confirmation_token}/, sent.body
|
51
56
|
end
|
52
57
|
|
53
58
|
When /^I follow the confirmation link sent to "(.*)"$/ do |email|
|
54
59
|
user = User.find_by_email(email)
|
55
|
-
visit new_user_confirmation_path(:user_id => user,
|
60
|
+
visit new_user_confirmation_path(:user_id => user,
|
61
|
+
:token => user.confirmation_token)
|
56
62
|
end
|
57
63
|
|
58
64
|
Then /^a password reset message should be sent to "(.*)"$/ do |email|
|
@@ -60,13 +66,14 @@ Then /^a password reset message should be sent to "(.*)"$/ do |email|
|
|
60
66
|
sent = ActionMailer::Base.deliveries.first
|
61
67
|
assert_equal [user.email], sent.to
|
62
68
|
assert_match /password/i, sent.subject
|
63
|
-
assert !user.
|
64
|
-
assert_match /#{user.
|
69
|
+
assert !user.confirmation_token.blank?
|
70
|
+
assert_match /#{user.confirmation_token}/, sent.body
|
65
71
|
end
|
66
72
|
|
67
73
|
When /^I follow the password reset link sent to "(.*)"$/ do |email|
|
68
74
|
user = User.find_by_email(email)
|
69
|
-
visit edit_user_password_path(:user_id => user,
|
75
|
+
visit edit_user_password_path(:user_id => user,
|
76
|
+
:token => user.confirmation_token)
|
70
77
|
end
|
71
78
|
|
72
79
|
When /^I try to change the password of "(.*)" without token$/ do |email|
|
@@ -80,11 +87,10 @@ end
|
|
80
87
|
|
81
88
|
# Actions
|
82
89
|
|
83
|
-
When /^I sign in
|
90
|
+
When /^I sign in as "(.*)\/(.*)"$/ do |email, password|
|
84
91
|
When %{I go to the sign in page}
|
85
92
|
And %{I fill in "Email" with "#{email}"}
|
86
93
|
And %{I fill in "Password" with "#{password}"}
|
87
|
-
And %{I check "Remember me"} if remember
|
88
94
|
And %{I press "Sign In"}
|
89
95
|
end
|
90
96
|
|
@@ -5,7 +5,7 @@
|
|
5
5
|
</p>
|
6
6
|
|
7
7
|
<% semantic_form_for(:user,
|
8
|
-
:url => user_password_path(@user, :token => @user.
|
8
|
+
:url => user_password_path(@user, :token => @user.confirmation_token),
|
9
9
|
:html => { :method => :put }) do |form| %>
|
10
10
|
<%= form.error_messages %>
|
11
11
|
<% form.inputs do -%>
|
@@ -1,7 +1,7 @@
|
|
1
1
|
module Clearance
|
2
2
|
module Authentication
|
3
3
|
|
4
|
-
def self.included(controller)
|
4
|
+
def self.included(controller) # :nodoc:
|
5
5
|
controller.send(:include, InstanceMethods)
|
6
6
|
|
7
7
|
controller.class_eval do
|
@@ -11,34 +11,79 @@ module Clearance
|
|
11
11
|
end
|
12
12
|
|
13
13
|
module InstanceMethods
|
14
|
+
# User in the current cookie
|
15
|
+
#
|
16
|
+
# @return [User, nil]
|
14
17
|
def current_user
|
15
|
-
@_current_user ||=
|
18
|
+
@_current_user ||= user_from_cookie
|
16
19
|
end
|
17
20
|
|
21
|
+
# Is the current user signed in?
|
22
|
+
#
|
23
|
+
# @return [true, false]
|
18
24
|
def signed_in?
|
19
25
|
! current_user.nil?
|
20
26
|
end
|
21
27
|
|
28
|
+
# Is the current user signed out?
|
29
|
+
#
|
30
|
+
# @return [true, false]
|
22
31
|
def signed_out?
|
23
32
|
current_user.nil?
|
24
33
|
end
|
25
34
|
|
26
|
-
|
27
|
-
|
35
|
+
# Deny the user access if they are signed out.
|
36
|
+
#
|
37
|
+
# @example
|
38
|
+
# before_filter :authenticate
|
28
39
|
def authenticate
|
29
40
|
deny_access unless signed_in?
|
30
41
|
end
|
31
42
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
43
|
+
# Sign user in to cookie.
|
44
|
+
#
|
45
|
+
# @param [User]
|
46
|
+
#
|
47
|
+
# @example
|
48
|
+
# sign_in(@user)
|
49
|
+
def sign_in(user)
|
50
|
+
if user
|
51
|
+
user.remember_me!
|
52
|
+
cookies[:remember_token] = {
|
53
|
+
:value => user.remember_token,
|
54
|
+
:expires => user.remember_token_expires_at
|
55
|
+
}
|
36
56
|
end
|
37
57
|
end
|
38
58
|
|
59
|
+
# Sign user out of cookie.
|
60
|
+
#
|
61
|
+
# @param [User]
|
62
|
+
#
|
63
|
+
# @example
|
64
|
+
# sign_out(@user)
|
65
|
+
def sign_out(user)
|
66
|
+
user.forget_me! if user
|
67
|
+
cookies.delete(:remember_token)
|
68
|
+
reset_session
|
69
|
+
end
|
70
|
+
|
71
|
+
# Store the current location.
|
72
|
+
# Display a flash message if included.
|
73
|
+
# Redirect to sign in.
|
74
|
+
#
|
75
|
+
# @param [String] optional flash message to display to denied user
|
76
|
+
def deny_access(flash_message = nil)
|
77
|
+
store_location
|
78
|
+
flash[:failure] = flash_message if flash_message
|
79
|
+
redirect_to(new_session_url)
|
80
|
+
end
|
81
|
+
|
82
|
+
protected
|
83
|
+
|
39
84
|
def user_from_cookie
|
40
85
|
if token = cookies[:remember_token]
|
41
|
-
return nil unless user = ::User.
|
86
|
+
return nil unless user = ::User.find_by_remember_token(token)
|
42
87
|
return user if user.remember?
|
43
88
|
end
|
44
89
|
end
|
@@ -48,26 +93,8 @@ module Clearance
|
|
48
93
|
sign_in(user)
|
49
94
|
end
|
50
95
|
|
51
|
-
def
|
52
|
-
if
|
53
|
-
session[:user_id] = user.id
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
def remember?
|
58
|
-
params[:session] && params[:session][:remember_me] == "1"
|
59
|
-
end
|
60
|
-
|
61
|
-
def remember(user)
|
62
|
-
user.remember_me!
|
63
|
-
cookies[:remember_token] = { :value => user.token,
|
64
|
-
:expires => user.token_expires_at }
|
65
|
-
end
|
66
|
-
|
67
|
-
def forget(user)
|
68
|
-
user.forget_me! if user
|
69
|
-
cookies.delete(:remember_token)
|
70
|
-
reset_session
|
96
|
+
def store_location
|
97
|
+
session[:return_to] = request.request_uri if request.get?
|
71
98
|
end
|
72
99
|
|
73
100
|
def redirect_back_or(default)
|
@@ -86,16 +113,6 @@ module Clearance
|
|
86
113
|
def redirect_to_root
|
87
114
|
redirect_to(root_url)
|
88
115
|
end
|
89
|
-
|
90
|
-
def store_location
|
91
|
-
session[:return_to] = request.request_uri if request.get?
|
92
|
-
end
|
93
|
-
|
94
|
-
def deny_access(flash_message = nil, opts = {})
|
95
|
-
store_location
|
96
|
-
flash[:failure] = flash_message if flash_message
|
97
|
-
redirect_to(new_session_url)
|
98
|
-
end
|
99
116
|
end
|
100
117
|
|
101
118
|
end
|
data/lib/clearance/user.rb
CHANGED
@@ -3,6 +3,23 @@ require 'digest/sha1'
|
|
3
3
|
module Clearance
|
4
4
|
module User
|
5
5
|
|
6
|
+
# Hook for all Clearance::User modules.
|
7
|
+
#
|
8
|
+
# If you need to override parts of Clearance::User,
|
9
|
+
# extend and include à la carte.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# extend ClassMethods
|
13
|
+
# include InstanceMethods
|
14
|
+
# include AttrAccessor
|
15
|
+
# include Callbacks
|
16
|
+
#
|
17
|
+
# @see ClassMethods
|
18
|
+
# @see InstanceMethods
|
19
|
+
# @see AttrAccessible
|
20
|
+
# @see AttrAccessor
|
21
|
+
# @see Validations
|
22
|
+
# @see Callbacks
|
6
23
|
def self.included(model)
|
7
24
|
model.extend(ClassMethods)
|
8
25
|
|
@@ -14,6 +31,15 @@ module Clearance
|
|
14
31
|
end
|
15
32
|
|
16
33
|
module AttrAccessible
|
34
|
+
# Hook for attr_accessible white list.
|
35
|
+
#
|
36
|
+
# :email, :password, :password_confirmation
|
37
|
+
#
|
38
|
+
# Append other attributes that must be mass-assigned in your model.
|
39
|
+
#
|
40
|
+
# @example
|
41
|
+
# include Clearance::User
|
42
|
+
# attr_accessible :location, :gender
|
17
43
|
def self.included(model)
|
18
44
|
model.class_eval do
|
19
45
|
attr_accessible :email, :password, :password_confirmation
|
@@ -22,6 +48,9 @@ module Clearance
|
|
22
48
|
end
|
23
49
|
|
24
50
|
module AttrAccessor
|
51
|
+
# Hook for attr_accessor virtual attributes.
|
52
|
+
#
|
53
|
+
# :password, :password_confirmation
|
25
54
|
def self.included(model)
|
26
55
|
model.class_eval do
|
27
56
|
attr_accessor :password, :password_confirmation
|
@@ -30,6 +59,12 @@ module Clearance
|
|
30
59
|
end
|
31
60
|
|
32
61
|
module Validations
|
62
|
+
# Hook for validations.
|
63
|
+
#
|
64
|
+
# :email must be present, unique, formatted
|
65
|
+
#
|
66
|
+
# If password is required,
|
67
|
+
# :password must be present, confirmed
|
33
68
|
def self.included(model)
|
34
69
|
model.class_eval do
|
35
70
|
validates_presence_of :email
|
@@ -43,50 +78,93 @@ module Clearance
|
|
43
78
|
end
|
44
79
|
|
45
80
|
module Callbacks
|
81
|
+
# Hook for callbacks.
|
82
|
+
#
|
83
|
+
# salt, token, password encryption are handled before_save.
|
46
84
|
def self.included(model)
|
47
85
|
model.class_eval do
|
48
|
-
before_save :initialize_salt,
|
86
|
+
before_save :initialize_salt,
|
87
|
+
:encrypt_password,
|
88
|
+
:initialize_confirmation_token
|
49
89
|
end
|
50
90
|
end
|
51
91
|
end
|
52
92
|
|
53
93
|
module InstanceMethods
|
94
|
+
# Am I authenticated with given password?
|
95
|
+
#
|
96
|
+
# @param [String] plain-text password
|
97
|
+
# @return [true, false]
|
98
|
+
# @example
|
99
|
+
# user.authenticated?('password')
|
54
100
|
def authenticated?(password)
|
55
101
|
encrypted_password == encrypt(password)
|
56
102
|
end
|
57
103
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
104
|
+
# Am I remembered?
|
105
|
+
#
|
106
|
+
# @return [true, false]
|
107
|
+
# @example
|
108
|
+
# user.remember?
|
62
109
|
def remember?
|
63
|
-
|
64
|
-
|
65
|
-
|
110
|
+
remember_token &&
|
111
|
+
remember_token_expires_at &&
|
112
|
+
Time.now.utc < remember_token_expires_at
|
113
|
+
end
|
114
|
+
|
115
|
+
# Remember me for a year.
|
116
|
+
#
|
117
|
+
# @example
|
118
|
+
# user.remember_me!
|
119
|
+
# cookies[:remember_token] = {
|
120
|
+
# :value => user.remember_token,
|
121
|
+
# :expires => user.remember_token_expires_at
|
122
|
+
# }
|
66
123
|
def remember_me!
|
67
|
-
remember_me_until!
|
124
|
+
remember_me_until! 1.year.from_now.utc
|
68
125
|
end
|
69
126
|
|
127
|
+
# Forget me.
|
128
|
+
#
|
129
|
+
# @example
|
130
|
+
# user.forget_me!
|
70
131
|
def forget_me!
|
71
|
-
|
132
|
+
self.remember_token = nil
|
133
|
+
self.remember_token_expires_at = nil
|
72
134
|
save(false)
|
73
135
|
end
|
74
136
|
|
137
|
+
# Confirm my email.
|
138
|
+
#
|
139
|
+
# @example
|
140
|
+
# user.confirm_email!
|
75
141
|
def confirm_email!
|
76
|
-
self.email_confirmed
|
77
|
-
self.
|
142
|
+
self.email_confirmed = true
|
143
|
+
self.confirmation_token = nil
|
78
144
|
save(false)
|
79
145
|
end
|
80
146
|
|
147
|
+
# Mark my account as forgotten password.
|
148
|
+
#
|
149
|
+
# @example
|
150
|
+
# user.forgot_password!
|
81
151
|
def forgot_password!
|
82
|
-
|
152
|
+
generate_confirmation_token
|
83
153
|
save(false)
|
84
154
|
end
|
85
155
|
|
156
|
+
# Update my password.
|
157
|
+
#
|
158
|
+
# @param [String, String] password and password confirmation
|
159
|
+
# @return [true, false] password was updated or not
|
160
|
+
# @example
|
161
|
+
# user.update_password('new-password', 'new-password')
|
86
162
|
def update_password(new_password, new_password_confirmation)
|
87
163
|
self.password = new_password
|
88
164
|
self.password_confirmation = new_password_confirmation
|
89
|
-
|
165
|
+
if valid?
|
166
|
+
self.confirmation_token = nil
|
167
|
+
end
|
90
168
|
save
|
91
169
|
end
|
92
170
|
|
@@ -98,7 +176,7 @@ module Clearance
|
|
98
176
|
|
99
177
|
def initialize_salt
|
100
178
|
if new_record?
|
101
|
-
self.salt = generate_hash("--#{Time.now.utc
|
179
|
+
self.salt = generate_hash("--#{Time.now.utc}--#{password}--")
|
102
180
|
end
|
103
181
|
end
|
104
182
|
|
@@ -107,18 +185,16 @@ module Clearance
|
|
107
185
|
self.encrypted_password = encrypt(password)
|
108
186
|
end
|
109
187
|
|
110
|
-
def
|
111
|
-
|
112
|
-
self.token_expires_at = nil
|
188
|
+
def encrypt(string)
|
189
|
+
generate_hash("--#{salt}--#{string}--")
|
113
190
|
end
|
114
191
|
|
115
|
-
def
|
116
|
-
self.
|
117
|
-
self.token_expires_at = nil
|
192
|
+
def generate_confirmation_token
|
193
|
+
self.confirmation_token = encrypt("--#{Time.now.utc}--#{password}--")
|
118
194
|
end
|
119
195
|
|
120
|
-
def
|
121
|
-
|
196
|
+
def initialize_confirmation_token
|
197
|
+
generate_confirmation_token if new_record?
|
122
198
|
end
|
123
199
|
|
124
200
|
def password_required?
|
@@ -126,13 +202,19 @@ module Clearance
|
|
126
202
|
end
|
127
203
|
|
128
204
|
def remember_me_until!(time)
|
129
|
-
self.
|
130
|
-
self.
|
205
|
+
self.remember_token_expires_at = time
|
206
|
+
self.remember_token = encrypt("--#{time}--#{password}--")
|
131
207
|
save(false)
|
132
208
|
end
|
133
209
|
end
|
134
210
|
|
135
211
|
module ClassMethods
|
212
|
+
# Authenticate with email and password.
|
213
|
+
#
|
214
|
+
# @param [String, String] email and password
|
215
|
+
# @return [User, nil] authenticated user or nil
|
216
|
+
# @example
|
217
|
+
# User.authenticate("email@example.com", "password")
|
136
218
|
def authenticate(email, password)
|
137
219
|
return nil unless user = find_by_email(email)
|
138
220
|
return user if user.authenticated?(password)
|
data/shoulda_macros/clearance.rb
CHANGED
@@ -4,6 +4,7 @@ module Clearance
|
|
4
4
|
# STATE OF AUTHENTICATION
|
5
5
|
|
6
6
|
def should_be_signed_in_as(&block)
|
7
|
+
warn "[DEPRECATION] should_be_signed_in_as cannot be used in functional tests anymore now that it depends on cookies, which are unavailable until the next request."
|
7
8
|
should "be signed in as #{block.bind(self).call}" do
|
8
9
|
user = block.bind(self).call
|
9
10
|
assert_not_nil user,
|
@@ -28,6 +29,7 @@ module Clearance
|
|
28
29
|
end
|
29
30
|
|
30
31
|
def should_not_be_signed_in
|
32
|
+
warn "[DEPRECATION] should_not_be_signed_in is no longer a valid test since we now store a remember_token in cookies, not user_id in session"
|
31
33
|
should "not be signed in" do
|
32
34
|
assert_nil session[:user_id]
|
33
35
|
end
|
@@ -176,7 +178,7 @@ module Clearance
|
|
176
178
|
warn "[DEPRECATION] should_display_a_password_update_form: not meant to be public, no longer used internally"
|
177
179
|
should "have a form for the user's token, password, and password confirm" do
|
178
180
|
update_path = ERB::Util.h(
|
179
|
-
user_password_path(@user, :token => @user.
|
181
|
+
user_password_path(@user, :token => @user.confirmation_token)
|
180
182
|
)
|
181
183
|
|
182
184
|
assert_select 'form[action=?]', update_path do
|
@@ -213,8 +215,6 @@ module Clearance
|
|
213
215
|
"session[email]", true, "There must be an email field"
|
214
216
|
assert_select "input[type=password][name=?]",
|
215
217
|
"session[password]", true, "There must be a password field"
|
216
|
-
assert_select "input[type=checkbox][name=?]",
|
217
|
-
"session[remember_me]", true, "There must be a 'remember me' check box"
|
218
218
|
assert_select "input[type=submit]", true,
|
219
219
|
"There must be a submit button"
|
220
220
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: thoughtbot-clearance
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dan Croak
|
@@ -24,7 +24,7 @@ autorequire:
|
|
24
24
|
bindir: bin
|
25
25
|
cert_chain: []
|
26
26
|
|
27
|
-
date: 2009-08-
|
27
|
+
date: 2009-08-30 21:00:00 -07:00
|
28
28
|
default_executable:
|
29
29
|
dependencies: []
|
30
30
|
|
@@ -115,8 +115,6 @@ files:
|
|
115
115
|
- rails/init.rb
|
116
116
|
has_rdoc: true
|
117
117
|
homepage: http://github.com/thoughtbot/clearance
|
118
|
-
licenses: []
|
119
|
-
|
120
118
|
post_install_message:
|
121
119
|
rdoc_options: []
|
122
120
|
|
@@ -137,7 +135,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
137
135
|
requirements: []
|
138
136
|
|
139
137
|
rubyforge_project:
|
140
|
-
rubygems_version: 1.
|
138
|
+
rubygems_version: 1.2.0
|
141
139
|
signing_key:
|
142
140
|
specification_version: 3
|
143
141
|
summary: Rails authentication with email & password.
|