josevalim-auth_helpers 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +17 -0
- data/MIT-LICENSE +20 -0
- data/README +175 -1
- data/init.rb +1 -0
- data/lib/auth_helpers/migration.rb +56 -0
- data/lib/auth_helpers/model/associatable.rb +47 -0
- data/lib/auth_helpers/model/authenticable.rb +105 -0
- data/lib/auth_helpers/model/confirmable.rb +85 -0
- data/lib/auth_helpers/model/recoverable.rb +74 -0
- data/lib/auth_helpers/model/rememberable.rb +65 -0
- data/lib/auth_helpers/model/validatable.rb +43 -0
- data/lib/auth_helpers/notifier.rb +54 -0
- data/lib/auth_helpers/spec/associatable.rb +34 -0
- data/lib/auth_helpers/spec/authenticable.rb +87 -0
- data/lib/auth_helpers/spec/confirmable.rb +101 -0
- data/lib/auth_helpers/spec/notifier.rb +43 -0
- data/lib/auth_helpers/spec/recoverable.rb +61 -0
- data/lib/auth_helpers/spec/rememberable.rb +77 -0
- data/lib/auth_helpers/spec/validatable.rb +29 -0
- data/lib/auth_helpers.rb +34 -0
- metadata +82 -0
data/CHANGELOG
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# Version 0.1
|
2
|
+
|
3
|
+
* First version with the following modules:
|
4
|
+
|
5
|
+
AuthHelpers::Model::Associatable
|
6
|
+
AuthHelpers::Model::Authenticable
|
7
|
+
AuthHelpers::Model::Confirmable
|
8
|
+
AuthHelpers::Model::Recoverable
|
9
|
+
AuthHelpers::Model::Rememberable
|
10
|
+
AuthHelpers::Model::Validatable
|
11
|
+
|
12
|
+
Plus:
|
13
|
+
|
14
|
+
AuthHelpers::Notifier
|
15
|
+
AuthHelpers::Migration
|
16
|
+
|
17
|
+
The first to deal with Notifications and the second with migrations.
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 José Valim
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,175 @@
|
|
1
|
+
AuthHelpers
|
2
|
+
License: MIT
|
3
|
+
Version: 0.1
|
4
|
+
|
5
|
+
You can also read this README in pretty html at the GitHub project Wiki page:
|
6
|
+
|
7
|
+
http://wiki.github.com/josevalim/auth_helpers
|
8
|
+
|
9
|
+
Description
|
10
|
+
-----------
|
11
|
+
|
12
|
+
AuthHelpers is a collection of modules to include in your model to deal with
|
13
|
+
authentication.
|
14
|
+
|
15
|
+
Why? Authentication is something that you need to do right since the beginning,
|
16
|
+
otherwise it will haunt you until the end of the project.
|
17
|
+
|
18
|
+
Gladly, Rails community has two awesome gems for this: Clearance and AuthLogic.
|
19
|
+
While the first gives you a simple but full, ready-to-go engine, the second
|
20
|
+
is a complex, fully featured authencation library.
|
21
|
+
|
22
|
+
While working in different projects, requisites change and sometimes you need
|
23
|
+
something that works between something simple and something fully featured. This
|
24
|
+
is the scope of AuthHelpers: you have modules and you include them where you
|
25
|
+
want.
|
26
|
+
|
27
|
+
Installation
|
28
|
+
------------
|
29
|
+
|
30
|
+
Install AuthHelpers is very easy. It is stored in GitHub, so just run the
|
31
|
+
following:
|
32
|
+
|
33
|
+
gem sources -a http://gems.github.com
|
34
|
+
sudo gem install josevalim-auth_helpers
|
35
|
+
|
36
|
+
If you want it as plugin, just do:
|
37
|
+
|
38
|
+
script/plugin install git://github.com/josevalim/auth_helpers.git
|
39
|
+
|
40
|
+
Modules
|
41
|
+
-------
|
42
|
+
|
43
|
+
class Account < ActiveRecord::Base
|
44
|
+
SALT = APP_NAME
|
45
|
+
|
46
|
+
include AuthHelpers::Model::Associatable
|
47
|
+
include AuthHelpers::Model::Authenticable
|
48
|
+
include AuthHelpers::Model::Confirmable
|
49
|
+
include AuthHelpers::Model::Recoverable
|
50
|
+
include AuthHelpers::Model::Rememberable
|
51
|
+
include AuthHelpers::Model::Validatable
|
52
|
+
end
|
53
|
+
|
54
|
+
== Associatable
|
55
|
+
|
56
|
+
This module automatically creates a belongs_to association for the first *_id
|
57
|
+
in the table. This module exists because, whenever it's possible, I follow the
|
58
|
+
pattern of having an account model and then all accountable objects uses this
|
59
|
+
model (it autodetects polymorphic associations too).
|
60
|
+
|
61
|
+
== Authenticable
|
62
|
+
|
63
|
+
It adds password, salt and encrypt behavior. Adds find_and_authenticate class
|
64
|
+
method and authenticate?(password) as instance method. It requires a constant
|
65
|
+
named SALT set in your model.
|
66
|
+
|
67
|
+
== Confirmable
|
68
|
+
|
69
|
+
Adds the confirmation_code handling. It sends an e-mail to the user on account
|
70
|
+
creation, and adds find_and_confirm, find_and_resend_confirmation_code as class
|
71
|
+
methods and confirmed? and confirm as instance methods.
|
72
|
+
|
73
|
+
When used with Authenticable, also sends an e-mail with a new confirmation code
|
74
|
+
whenever the user changes his e-mail address.
|
75
|
+
|
76
|
+
== Recoverable
|
77
|
+
|
78
|
+
Adds the reset_password_code handling. Adds find_and_resend_confirmation_code
|
79
|
+
and find_and_reset_password class methods.
|
80
|
+
|
81
|
+
== Rememberable
|
82
|
+
|
83
|
+
Manages a token to be stored in cookies. Adds find_by_remember_me_token (which
|
84
|
+
only returns a record if the token hasn't expired yet), remember_me! and forget_me!
|
85
|
+
methods.
|
86
|
+
|
87
|
+
Whenever used with Authentication, it handles the remember_me method inside
|
88
|
+
find_and_authenticate.
|
89
|
+
|
90
|
+
== Validatable
|
91
|
+
|
92
|
+
Add validations to your e-mail and password. If you have a constant in your model
|
93
|
+
named SCOPE, it will add this to validate_uniqueness_of.
|
94
|
+
|
95
|
+
Specs
|
96
|
+
-----
|
97
|
+
|
98
|
+
All those modules comes with specs, that's why the library has not tests per se.
|
99
|
+
So if you want to test the Account model declared above, just do:
|
100
|
+
|
101
|
+
describe Account do
|
102
|
+
include AuthHelpers::Spec::Associatable
|
103
|
+
include AuthHelpers::Spec::Authenticable
|
104
|
+
include AuthHelpers::Spec::Confirmable
|
105
|
+
include AuthHelpers::Spec::Recoverable
|
106
|
+
include AuthHelpers::Spec::Rememberable
|
107
|
+
include AuthHelpers::Spec::Validatable
|
108
|
+
|
109
|
+
before(:each) do
|
110
|
+
@valid_attributes = {
|
111
|
+
:email => "is.valid@email.com",
|
112
|
+
:email_confirmation => "is.valid@email.com",
|
113
|
+
:password => "abcdef",
|
114
|
+
:password_confirmation => "abcdef"
|
115
|
+
}
|
116
|
+
end
|
117
|
+
|
118
|
+
it "should create a new instance given valid attributes" do
|
119
|
+
Account.create!(@valid_attributes)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
The only requisite you have for the tests is to have a @valid_attributes instance
|
124
|
+
variable set with a hash of valid attributes. You also need Remarkable to run
|
125
|
+
those tests.
|
126
|
+
|
127
|
+
Migrations
|
128
|
+
----------
|
129
|
+
|
130
|
+
While AuthHelpers gives you the flexibity to choose which model you want to add
|
131
|
+
your validations, it takes from you the freedom to choose what are the column
|
132
|
+
names. However it makes easier to create your migrations. This is a migration up
|
133
|
+
example for the Account model above:
|
134
|
+
|
135
|
+
create_table :accounts do |t|
|
136
|
+
t.references :accountable, :polymorphic => true
|
137
|
+
t.extend AuthHelpers::Migration
|
138
|
+
|
139
|
+
t.authenticable
|
140
|
+
t.confirmable
|
141
|
+
t.recoverable
|
142
|
+
t.rememberable
|
143
|
+
t.timestamps
|
144
|
+
end
|
145
|
+
|
146
|
+
Notifications
|
-
|
|
147
|
+
|
148
|
+
AuthHelpers also comes with default notification files. At some point you will
|
149
|
+
want to prettify your notification views, so you just need to do:
|
150
|
+
|
151
|
+
AuthHelpers::Notifier.sender = %("José Valim" <jose.valim@gmail.com>)
|
152
|
+
AuthHelpers::Notifier.template_root = "#{RAILS_ROOT}/app/views"
|
153
|
+
|
154
|
+
Then make a copy of the plugin views folder to your app/views and start to work
|
155
|
+
on them.
|
156
|
+
|
157
|
+
Need more?
|
158
|
+
----------
|
159
|
+
|
160
|
+
I'm open to extend this library to include more modules. So if you want an
|
161
|
+
Invitable module, fork the project, add it and then send it to me. I will pull
|
162
|
+
it in gladly.
|
163
|
+
|
164
|
+
Example app
|
165
|
+
-----------
|
166
|
+
|
167
|
+
http://github.com/josevalim/starter
|
168
|
+
|
169
|
+
Bugs and Feedback
|
170
|
+
-----------------
|
171
|
+
|
172
|
+
If you discover any bugs, please send an e-mail to jose.valim@gmail.com
|
173
|
+
|
174
|
+
Copyright (c) 2009 José Valim
|
175
|
+
http://josevalim.blogspot.com/
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'lib', 'auth_helpers')
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module AuthHelpers
|
2
|
+
# Helpers to migration:
|
3
|
+
#
|
4
|
+
# create_table :accounts do |t|
|
5
|
+
# t.extend AuthHelpers::Migration
|
6
|
+
#
|
7
|
+
# t.authenticable
|
8
|
+
# t.confirmable
|
9
|
+
# t.recoverable
|
10
|
+
# t.rememberable
|
11
|
+
# t.timestamps
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# However this method does not add indexes. If you need them, here is the declaration:
|
15
|
+
#
|
16
|
+
# add_index "accounts", ["email"], :name => "email", :unique => true
|
17
|
+
# add_index "accounts", ["token"], :name => "token", :unique => true
|
18
|
+
# add_index "accounts", ["confirmation_code"], :name => "confirmation_code", :unique => true
|
19
|
+
# add_index "accounts", ["reset_password_code"], :name => "reset_password_code", :unique => true
|
20
|
+
#
|
21
|
+
# E-mail index should be slightly changed with you are working with polymorphic
|
22
|
+
# associations.
|
23
|
+
#
|
24
|
+
module Migration
|
25
|
+
|
26
|
+
# Creates email, hashed_password and salt.
|
27
|
+
#
|
28
|
+
def authenticable
|
29
|
+
self.string :email, :limit => 100, :null => false, :default => ''
|
30
|
+
self.string :hashed_password, :limit => 40, :null => false, :default => ''
|
31
|
+
self.string :salt, :limit => 10, :null => false, :default => ''
|
32
|
+
end
|
33
|
+
|
34
|
+
# Creates confirmation_code, confirmed_at and confirmation_sent_at.
|
35
|
+
#
|
36
|
+
def confirmable
|
37
|
+
self.string :confirmation_code, :limit => 40, :null => true
|
38
|
+
self.datetime :confirmed_at
|
39
|
+
self.datetime :confirmation_sent_at
|
40
|
+
end
|
41
|
+
|
42
|
+
# Creates reset_password_code.
|
43
|
+
#
|
44
|
+
def recoverable
|
45
|
+
self.string :reset_password_code, :limit => 40, :null => true
|
46
|
+
end
|
47
|
+
|
48
|
+
# Creates token and token_created_at.
|
49
|
+
#
|
50
|
+
def rememberable
|
51
|
+
self.string :token, :limit => 40, :null => true
|
52
|
+
self.datetime :token_expires_at
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module AuthHelpers
|
2
|
+
module Model
|
3
|
+
|
4
|
+
# Checks for a column that ends with _id in the included model. Then it adds
|
5
|
+
# a belongs_to association, accepts_nested_attributes_for and make the nested
|
6
|
+
# attributes accessible.
|
7
|
+
#
|
8
|
+
# Also includes a hook called remove_association_error, that removes the nested
|
9
|
+
# attribute errors from the parent object.
|
10
|
+
#
|
11
|
+
# Finally, if the *_id in the table has also *_type. It considers a polymorphic
|
12
|
+
# association.
|
13
|
+
#
|
14
|
+
module Associatable
|
15
|
+
def self.included(base)
|
16
|
+
column = base.columns.detect{|c| c.name =~ /_id$/ }
|
17
|
+
raise ScriptError, "Could not find a column that ends with id in #{base.name.tableize}" unless column
|
18
|
+
|
19
|
+
association = column.name.gsub(/_id$/, '').to_sym
|
20
|
+
polymorphic = !!base.columns.detect{ |c| c.name == "#{association}_type" }
|
21
|
+
|
22
|
+
base.class_eval do
|
23
|
+
belongs_to association, :validate => true, :dependent => :destroy,
|
24
|
+
:autosave => true, :polymorphic => polymorphic
|
25
|
+
|
26
|
+
accepts_nested_attributes_for association
|
27
|
+
attr_accessible :"#{association}_attributes"
|
28
|
+
|
29
|
+
after_validation :remove_association_error
|
30
|
+
end
|
31
|
+
|
32
|
+
base.class_eval <<-ASSOCIATION
|
33
|
+
# Remove association errors from the message
|
34
|
+
#
|
35
|
+
def remove_association_error
|
36
|
+
self.errors.each do |key, value|
|
37
|
+
next unless key.to_s =~ /^#{association}_/
|
38
|
+
self.errors.instance_variable_get('@errors').delete(key)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
protected :remove_association_error
|
42
|
+
ASSOCIATION
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'digest/sha1'
|
2
|
+
require File.join(File.dirname(__FILE__), '..', 'notifier')
|
3
|
+
|
4
|
+
module AuthHelpers
|
5
|
+
module Model
|
6
|
+
|
7
|
+
# Adds methods that helps you to authenticate an user. It requires that you set
|
8
|
+
# a constant called SALT in your model.
|
9
|
+
#
|
10
|
+
module Authenticable
|
11
|
+
def self.included(base)
|
12
|
+
base.send :attr_accessor, :email_confirmation, :password_confirmation
|
13
|
+
base.send :attr_accessible, :email, :email_confirmation, :password, :password_confirmation
|
14
|
+
base.extend ClassMethods
|
15
|
+
end
|
16
|
+
|
17
|
+
# Overwrite update attributes to deal with email, password and confirmations.
|
18
|
+
#
|
19
|
+
def update_attributes(options)
|
20
|
+
# Reject email if it didn't change or is blank
|
21
|
+
options.delete(:email) if options[:email].blank? || options[:email] == self.email
|
22
|
+
options.delete(:email_confirmation) if options[:email_confirmation].blank?
|
23
|
+
|
24
|
+
# Reject password if it didn't change or is blank
|
25
|
+
options.delete(:password) if options[:password].blank? || self.authenticate?(options[:password])
|
26
|
+
options.delete(:password_confirmation) if options[:password_confirmation].blank?
|
27
|
+
|
28
|
+
# Force confirmations (if confirmation is nil, it won't validate, it has to be at least blank)
|
29
|
+
options[:email_confirmation] ||= '' if options[:email]
|
30
|
+
options[:password_confirmation] ||= '' if options[:password]
|
31
|
+
|
32
|
+
if super(options)
|
33
|
+
# Generate a new confirmation code, save and send it.
|
34
|
+
if options[:email] && respond_to?(:set_confirmation_code)
|
35
|
+
self.set_confirmation_code
|
36
|
+
self.save(false)
|
37
|
+
|
38
|
+
AuthHelpers::Notifier.deliver_email_changed(self)
|
39
|
+
end
|
40
|
+
|
41
|
+
return true
|
42
|
+
end
|
43
|
+
|
44
|
+
return false
|
45
|
+
end
|
46
|
+
|
47
|
+
# Authenticate the account by encrypting the password sent and comparing with the hashed password.
|
48
|
+
#
|
49
|
+
def authenticate?(auth_password)
|
50
|
+
self.hashed_password.not_blank? && self.class.send(:encrypt, auth_password, self.salt) == self.hashed_password
|
51
|
+
end
|
52
|
+
|
53
|
+
# Get the password
|
54
|
+
#
|
55
|
+
def password
|
56
|
+
@password
|
57
|
+
end
|
58
|
+
|
59
|
+
# Sets the password for this account by creating a salt and encrypting the password sent.
|
60
|
+
#
|
61
|
+
def password=(new_password)
|
62
|
+
@password = new_password
|
63
|
+
|
64
|
+
self.salt = AuthHelpers.random_string(10)
|
65
|
+
self.hashed_password = self.class.send(:encrypt, @password, self.salt)
|
66
|
+
end
|
67
|
+
|
68
|
+
module ClassMethods
|
69
|
+
|
70
|
+
# Finds and authenticate an record, setting error messages in case the object
|
71
|
+
# can't be authenticated.
|
72
|
+
#
|
73
|
+
# Account.find_and_authenticate(:email => 'my@email.com', :password => '123456')
|
74
|
+
#
|
75
|
+
def find_and_authenticate(options={})
|
76
|
+
authenticable = AuthHelpers.find_or_initialize_by_unless_blank(self, :email, options[:email])
|
77
|
+
|
78
|
+
unless authenticable.authenticate?(options[:password])
|
79
|
+
if options[:email].blank?
|
80
|
+
authenticable.errors.add :email, :blank
|
81
|
+
elsif options[:password].blank?
|
82
|
+
authenticable.errors.add :password, :blank
|
83
|
+
elsif authenticable.new_record?
|
84
|
+
authenticable.errors.add :email, :not_found, :email => options[:email]
|
85
|
+
else
|
86
|
+
authenticable.errors.add :password, :invalid, :email => options[:email]
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
return authenticable
|
91
|
+
end
|
92
|
+
|
93
|
+
protected
|
94
|
+
|
95
|
+
# Encrypts a string using a fixed salt and a variable salt.
|
96
|
+
#
|
97
|
+
def encrypt(password, salt)
|
98
|
+
return nil if password.blank? || salt.blank?
|
99
|
+
Digest::SHA1.hexdigest(password + self::SALT + salt)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'notifier')
|
2
|
+
|
3
|
+
module AuthHelpers
|
4
|
+
module Model
|
5
|
+
|
6
|
+
# Adds a module that deals with confirmations.
|
7
|
+
#
|
8
|
+
module Confirmable
|
9
|
+
def self.included(base)
|
10
|
+
base.extend ClassMethods
|
11
|
+
base.send :before_create, :set_confirmation_code
|
12
|
+
base.send :after_create, :send_new_account_notification
|
13
|
+
end
|
14
|
+
|
15
|
+
# Returns true if is not a new record and the confirmation code is blank.
|
16
|
+
#
|
17
|
+
def confirmed?
|
18
|
+
!self.new_record? && self.confirmation_code.blank?
|
19
|
+
end
|
20
|
+
|
21
|
+
# Confirms an account by setting :confirmation_code to nil and setting the
|
22
|
+
# confirmed_at field.
|
23
|
+
#
|
24
|
+
def confirm!
|
25
|
+
self.confirmation_code = nil
|
26
|
+
self.confirmed_at = Time.now.utc
|
27
|
+
return self.save(false)
|
28
|
+
end
|
29
|
+
|
30
|
+
protected
|
31
|
+
|
32
|
+
# Generates a confirmation_code and sets confirmation_sent_at.
|
33
|
+
# Does not save the object because it's used as a filter.
|
34
|
+
#
|
35
|
+
def set_confirmation_code
|
36
|
+
self.confirmation_code = AuthHelpers.generate_unique_string_for(self.class, :confirmation_code, 40)
|
37
|
+
self.confirmation_sent_at = Time.now.utc
|
38
|
+
return true
|
39
|
+
end
|
40
|
+
|
41
|
+
# Send a notification to new account. Used as filter.
|
42
|
+
#
|
43
|
+
def send_new_account_notification
|
44
|
+
AuthHelpers::Notifier.deliver_new_account(self)
|
45
|
+
end
|
46
|
+
|
47
|
+
module ClassMethods
|
48
|
+
|
49
|
+
# Receives a confirmation code, find the respective account and tries to set its confirmation code to nil.
|
50
|
+
# If something goes wrong, return an account object with errors.
|
51
|
+
#
|
52
|
+
def find_and_confirm(sent_confirmation_code)
|
53
|
+
confirmable = AuthHelpers.find_or_initialize_by_unless_blank(self, :confirmation_code, sent_confirmation_code)
|
54
|
+
|
55
|
+
if confirmable.new_record?
|
56
|
+
confirmable.errors.add :confirmation_code, :invalid
|
57
|
+
else
|
58
|
+
confirmable.confirm!
|
59
|
+
end
|
60
|
+
|
61
|
+
return confirmable
|
62
|
+
end
|
63
|
+
|
64
|
+
# Receives a hash with email and tries to find the account to resend the confirmation code.
|
65
|
+
# If the e-mail can't be found or the account is already confirmed, return an account object
|
66
|
+
# with errors.
|
67
|
+
#
|
68
|
+
def find_and_resend_confirmation_code(options = {})
|
69
|
+
confirmable = AuthHelpers.find_or_initialize_by_unless_blank(self, :email, options[:email])
|
70
|
+
|
71
|
+
if confirmable.new_record?
|
72
|
+
confirmable.errors.add :email, :not_found
|
73
|
+
elsif confirmable.confirmed?
|
74
|
+
confirmable.errors.add :email, :already_confirmed
|
75
|
+
else
|
76
|
+
AuthHelpers::Notifier.deliver_confirmation_code(confirmable)
|
77
|
+
end
|
78
|
+
|
79
|
+
return confirmable
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'notifier')
|
2
|
+
|
3
|
+
module AuthHelpers
|
4
|
+
module Model
|
5
|
+
|
6
|
+
# Adds a module that deals with forgot your password.
|
7
|
+
#
|
8
|
+
module Recoverable
|
9
|
+
def self.included(base)
|
10
|
+
base.send(:attr_accessible, :reset_password_code)
|
11
|
+
base.extend ClassMethods
|
12
|
+
end
|
13
|
+
|
14
|
+
# Reset the password with the new_password is equals its confirmation and
|
15
|
+
# set reset password code to nil.
|
16
|
+
#
|
17
|
+
def reset_password!(new_password, new_password_confirmation)
|
18
|
+
self.password = new_password
|
19
|
+
self.password_confirmation = new_password_confirmation
|
20
|
+
|
21
|
+
if self.valid?
|
22
|
+
self.reset_password_code = nil
|
23
|
+
return self.save
|
24
|
+
end
|
25
|
+
|
26
|
+
false
|
27
|
+
end
|
28
|
+
|
29
|
+
# Set a reset password code in the database and send it through e-mail
|
30
|
+
#
|
31
|
+
def send_reset_password_code
|
32
|
+
new_code = AuthHelpers.generate_unique_string_for(self.class, :reset_password_code, 40)
|
33
|
+
self.update_attribute(:reset_password_code, new_code)
|
34
|
+
|
35
|
+
AuthHelpers::Notifier.deliver_reset_password(self)
|
36
|
+
return true
|
37
|
+
end
|
38
|
+
|
39
|
+
module ClassMethods
|
40
|
+
|
41
|
+
# Receives a hash with reset_password_code, password and password confirmation.
|
42
|
+
# Tries to find the account with the sent password code, and then, if password and password
|
43
|
+
# confirmation matches, changes the password. Otherwise return an account object with errors.
|
44
|
+
#
|
45
|
+
def find_and_reset_password(options={})
|
46
|
+
recoverable = AuthHelpers.find_or_initialize_by_unless_blank(self, :reset_password_code, options[:reset_password_code])
|
47
|
+
|
48
|
+
if recoverable.new_record?
|
49
|
+
recoverable.errors.add :reset_password_code, :invalid
|
50
|
+
else
|
51
|
+
recoverable.reset_password!(options[:password], options[:password_confirmation])
|
52
|
+
end
|
53
|
+
|
54
|
+
return recoverable
|
55
|
+
end
|
56
|
+
|
57
|
+
# Receives a hash with email and tries to find the account to send a new reset password code.
|
58
|
+
# If the e-mail can't be found return an account object with errors.
|
59
|
+
#
|
60
|
+
def find_and_send_reset_password_code(options={})
|
61
|
+
recoverable = AuthHelpers.find_or_initialize_by_unless_blank(self, :email, options[:email])
|
62
|
+
|
63
|
+
if recoverable.new_record?
|
64
|
+
recoverable.errors.add :email, :not_found, options
|
65
|
+
else
|
66
|
+
recoverable.send_reset_password_code
|
67
|
+
end
|
68
|
+
|
69
|
+
return recoverable
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'notifier')
|
2
|
+
|
3
|
+
module AuthHelpers
|
4
|
+
module Model
|
5
|
+
|
6
|
+
# Adds remember_me to the model. The token is valid for two weeks, you can
|
7
|
+
# can change this by overwriting the token_expiration_interval method.
|
8
|
+
#
|
9
|
+
module Rememberable
|
10
|
+
def self.included(base)
|
11
|
+
base.extend ClassMethods
|
12
|
+
base.class_eval do
|
13
|
+
attr_accessor :remember_me
|
14
|
+
attr_accessible :remember_me
|
15
|
+
alias :remember_me? :remember_me
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Call to set and save a remember me token
|
20
|
+
#
|
21
|
+
def remember_me!
|
22
|
+
self.token = AuthHelpers.generate_unique_string_for(self.class, :token, 40)
|
23
|
+
self.token_expires_at = token_expiration_interval
|
24
|
+
self.save(false)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Call to forget and save the token
|
28
|
+
#
|
29
|
+
def forget_me!
|
30
|
+
self.token = nil
|
31
|
+
self.token_expires_at = nil
|
32
|
+
self.save(false)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Change if you want to set another token_expiration_interval or add
|
36
|
+
# custom logic (admin has one day token, clients have 2 weeks).
|
37
|
+
#
|
38
|
+
def token_expiration_interval
|
39
|
+
2.weeks.from_now
|
40
|
+
end
|
41
|
+
|
42
|
+
module ClassMethods
|
43
|
+
# Find the user with the given token only if it has not expired at.
|
44
|
+
#
|
45
|
+
def find_by_remember_me_token(token)
|
46
|
+
self.find(:first, :conditions => [ "token = ? AND token_expires_at > CURRENT_TIMESTAMP", token ])
|
47
|
+
end
|
48
|
+
|
49
|
+
# Overwrites find and authenticate to deal with the remember me key.
|
50
|
+
#
|
51
|
+
def find_and_authenticate(options={})
|
52
|
+
rememberable, remember_me = options.key?(:remember_me), options.delete(:remember_me)
|
53
|
+
authenticable = super(options)
|
54
|
+
|
55
|
+
if rememberable && authenticable.errors.empty?
|
56
|
+
remember_me == '1' ? authenticable.remember_me! : authenticable.forget_me!
|
57
|
+
end
|
58
|
+
|
59
|
+
authenticable
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module AuthHelpers
|
2
|
+
module Model
|
3
|
+
|
4
|
+
# Include validations.
|
5
|
+
#
|
6
|
+
# If you want to scope the validate uniqueness of, you have to set a constant
|
7
|
+
# SCOPE in your class.
|
8
|
+
#
|
9
|
+
# class Account < ActiveRecord::Base
|
10
|
+
# SALT = 'my_project_salt'
|
11
|
+
# SCOPE = [ :company_id ]
|
12
|
+
#
|
13
|
+
# include AuthHelpers::Models::Authenticable
|
14
|
+
# include AuthHelpers::Models::Validatable
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# Another hook provided is the password_required? method. It always returns
|
18
|
+
# true, but you can overwrite it to add custom logic.
|
19
|
+
#
|
20
|
+
module Validatable
|
21
|
+
EMAIL_REGEXP = /^([^@"'><&\s\,\;]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i
|
22
|
+
|
23
|
+
def self.included(base)
|
24
|
+
base.class_eval do
|
25
|
+
validates_presence_of :email
|
26
|
+
validates_length_of :email, :maximum => 100, :allow_blank => true
|
27
|
+
validates_format_of :email, :with => EMAIL_REGEXP, :allow_blank => true
|
28
|
+
validates_uniqueness_of :email, :case_sensitive => false, :allow_blank => true,
|
29
|
+
:scope => (defined?(base::SCOPE) ? base::SCOPE : [])
|
30
|
+
validates_confirmation_of :email
|
31
|
+
|
32
|
+
validates_presence_of :password, :if => :password_required?
|
33
|
+
validates_length_of :password, :within => 6..20, :allow_blank => true
|
34
|
+
validates_confirmation_of :password
|
35
|
+
|
36
|
+
# Overwrite if password is not required or implement custom logic
|
37
|
+
def password_required?; true; end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module AuthHelpers
|
2
|
+
# The class responsable to send e-mails.
|
3
|
+
#
|
4
|
+
# It uses default views in the auth_helpers/views. If you want to customize
|
5
|
+
# them, just do:
|
6
|
+
#
|
7
|
+
# AuthHelpers::Notifier.template_root = "#{RAILS_ROOT}/app/views"
|
8
|
+
#
|
9
|
+
# And put your new views at: "RAILS_ROOT/app/views/auth_helpers/notifier/"
|
10
|
+
#
|
11
|
+
# You should also configure the sender and content_type:
|
12
|
+
#
|
13
|
+
# AuthHelpers::Notifier.sender = %("José Valim" <jose.valim@gmail.com>)
|
14
|
+
# AuthHelpers::Notifier.content_type = 'text/html'
|
15
|
+
#
|
16
|
+
class Notifier < ActionMailer::Base
|
17
|
+
class << self; attr_accessor :sender, :content_type end
|
18
|
+
|
19
|
+
self.content_type = 'text/html'
|
20
|
+
self.template_root = File.join(File.dirname(__FILE__), '..', '..', 'views')
|
21
|
+
|
22
|
+
def new_account(record)
|
23
|
+
@subject = I18n.t 'actionmailer.auth_helpers.new_account', :default => 'New account'
|
24
|
+
set_ivars!(:confirmable, record)
|
25
|
+
end
|
26
|
+
|
27
|
+
def email_changed(record)
|
28
|
+
@subject = I18n.t 'actionmailer.auth_helpers.email_changed', :default => 'You changed your e-mail'
|
29
|
+
set_ivars!(:confirmable, record)
|
30
|
+
end
|
31
|
+
|
32
|
+
def reset_password(record)
|
33
|
+
@subject = I18n.t 'actionmailer.auth_helpers.reset_password', :default => 'Reset password'
|
34
|
+
set_ivars!(:recoverable, record)
|
35
|
+
end
|
36
|
+
|
37
|
+
def confirmation_code(record)
|
38
|
+
@subject = I18n.t 'actionmailer.auth_helpers.confirmation_code', :default => 'Confirmation code'
|
39
|
+
set_ivars!(:confirmable, record)
|
40
|
+
end
|
41
|
+
|
42
|
+
protected
|
43
|
+
|
44
|
+
def set_ivars!(assign, record)
|
45
|
+
@from = self.class.sender
|
46
|
+
@content_type = self.class.content_type
|
47
|
+
@body[assign] = record
|
48
|
+
@recipients = record.email
|
49
|
+
@sent_on = Time.now.utc
|
50
|
+
@headers = {}
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module AuthHelpers
|
2
|
+
module Spec
|
3
|
+
|
4
|
+
module Associatable
|
5
|
+
def self.included(base)
|
6
|
+
klass = base.described_class
|
7
|
+
|
8
|
+
column = klass.columns.detect{|c| c.name =~ /_id$/ }
|
9
|
+
raise ScriptError, "Could not find a column that ends with id in #{base.name.tableize}" unless column
|
10
|
+
|
11
|
+
association = column.name.gsub(/_id$/, '').to_sym
|
12
|
+
polymorphic = !!klass.columns.detect{ |c| c.name == "#{association}_type" }
|
13
|
+
|
14
|
+
base.class_eval do
|
15
|
+
should_belong_to association, :validate => true, :dependent => :destroy,
|
16
|
+
:autosave => true, :polymorphic => polymorphic
|
17
|
+
|
18
|
+
it "should validate associated #{association}" do
|
19
|
+
associatable = base.described_class.create(@valid_attributes.merge(:"#{association}_attributes" => {}))
|
20
|
+
associatable.should_not be_valid
|
21
|
+
|
22
|
+
unless associatable.send(association).errors.empty?
|
23
|
+
associatable.errors.should be_empty # this should be blank since errors is
|
24
|
+
# on the associated object.
|
25
|
+
|
26
|
+
associatable.send(association).errors.should_not be_empty
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module AuthHelpers
|
2
|
+
module Spec
|
3
|
+
|
4
|
+
module Authenticable
|
5
|
+
def self.included(base)
|
6
|
+
base.class_eval do
|
7
|
+
describe 'on authentication' do
|
8
|
+
it { should_not allow_mass_assignment_of(:salt, :hashed_password) }
|
9
|
+
|
10
|
+
it "should set a salt and hashed_password when assigning password" do
|
11
|
+
salt_value = '0123456789'
|
12
|
+
password_value = 'abcdef'
|
13
|
+
hashed_password_value = 'nice' * 10
|
14
|
+
|
15
|
+
AuthHelpers.should_receive(:random_string).with(10).and_return(salt_value)
|
16
|
+
base.described_class.should_receive(:encrypt).with(password_value, salt_value).and_return(hashed_password_value)
|
17
|
+
|
18
|
+
authenticable = base.described_class.new
|
19
|
+
authenticable.password = password_value
|
20
|
+
|
21
|
+
authenticable.salt.should == salt_value
|
22
|
+
authenticable.hashed_password.should == hashed_password_value
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should authenticate users with valid password" do
|
26
|
+
authenticable = base.described_class.new
|
27
|
+
authenticable.authenticate?('abcdef').should be_false
|
28
|
+
|
29
|
+
authenticable.password = 'abcdef'
|
30
|
+
authenticable.authenticate?(nil).should be_false
|
31
|
+
authenticable.authenticate?('notvalid').should be_false
|
32
|
+
authenticable.authenticate?('abcdef').should be_true
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should find and authenticate an account by email" do
|
36
|
+
base.described_class.create!(@valid_attributes)
|
37
|
+
|
38
|
+
authenticable = base.described_class.find_and_authenticate(:email => @valid_attributes[:email], :password => @valid_attributes[:password])
|
39
|
+
authenticable.errors.should be_empty
|
40
|
+
|
41
|
+
authenticable = base.described_class.find_and_authenticate(:email => @valid_attributes[:email], :password => @valid_attributes[:password].to_s.reverse)
|
42
|
+
authenticable.errors.on(:password).should == authenticable.errors.generate_message(:password, :invalid, @valid_attributes)
|
43
|
+
|
44
|
+
authenticable = base.described_class.find_and_authenticate(:email => @valid_attributes[:email], :password => '')
|
45
|
+
authenticable.errors.on(:password).should == authenticable.errors.generate_message(:password, :blank)
|
46
|
+
|
47
|
+
authenticable = base.described_class.find_and_authenticate(:email => 'does.not.exist@email.com', :password => @valid_attributes[:password].to_s.reverse)
|
48
|
+
authenticable.new_record?.should be_true
|
49
|
+
authenticable.errors.on(:email).should == authenticable.errors.generate_message(:email, :not_found)
|
50
|
+
|
51
|
+
authenticable = base.described_class.find_and_authenticate(:email => '', :password => 'notvalid')
|
52
|
+
authenticable.new_record?.should be_true
|
53
|
+
authenticable.errors.on(:email).should == authenticable.errors.generate_message(:email, :blank)
|
54
|
+
end
|
55
|
+
|
56
|
+
describe "on update" do
|
57
|
+
before(:each) do
|
58
|
+
@authenticable = base.described_class.create!(@valid_attributes)
|
59
|
+
ActionMailer::Base.deliveries = []
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should ignore e-mail confirmation if e-mail has not changed" do
|
63
|
+
attributes = { :email => @authenticable.email, :email_confirmation => '' }
|
64
|
+
@authenticable.update_attributes(attributes).should be_true
|
65
|
+
end
|
66
|
+
|
67
|
+
it "should ignore password confirmation if password has not changed" do
|
68
|
+
attributes = { :password => @valid_attributes[:password], :password_confirmation => '' }
|
69
|
+
@authenticable.update_attributes(attributes).should be_true
|
70
|
+
end
|
71
|
+
|
72
|
+
if base.described_class.new.respond_to?(:set_confirmation_code, true)
|
73
|
+
it "should send an e-mail if e-mail changes" do
|
74
|
+
attributes = { :email => @valid_attributes[:email].to_s.next, :email_confirmation => @valid_attributes[:email].to_s.next }
|
75
|
+
@authenticable.update_attributes(attributes).should be_true
|
76
|
+
@authenticable.email.should == @valid_attributes[:email].to_s.next
|
77
|
+
ActionMailer::Base.deliveries.length.should == 1
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module AuthHelpers
|
2
|
+
module Spec
|
3
|
+
|
4
|
+
module Confirmable
|
5
|
+
def self.included(base)
|
6
|
+
base.class_eval do
|
7
|
+
describe 'confirmation' do
|
8
|
+
before(:each) do
|
9
|
+
ActionMailer::Base.deliveries = []
|
10
|
+
@confirmable = base.described_class.create!(@valid_attributes)
|
11
|
+
end
|
12
|
+
|
13
|
+
it { should_not allow_mass_assignment_of(:confirmation_code) }
|
14
|
+
|
15
|
+
it 'should remove confirmation code' do
|
16
|
+
@confirmable.confirm!
|
17
|
+
@confirmable.confirmation_code.should be_nil
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'should set the date account was confirmed' do
|
21
|
+
@confirmable.confirmed_at.should be_nil
|
22
|
+
@confirmable.confirm!
|
23
|
+
@confirmable.confirmed_at.should_not be_nil
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should say when a record is confirmed or not" do
|
27
|
+
base.described_class.new.confirmed?.should be_false
|
28
|
+
@confirmable.confirmed?.should be_false
|
29
|
+
|
30
|
+
@confirmable.confirm!
|
31
|
+
@confirmable.confirmed?.should be_true
|
32
|
+
end
|
33
|
+
|
34
|
+
describe 'on create' do
|
35
|
+
it "should set confirmation_code" do
|
36
|
+
@confirmable.confirmation_code.length.should == 40
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should set confirmation_sent_at" do
|
40
|
+
@confirmable.confirmation_sent_at.should_not be_blank
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should send a new account notification" do
|
44
|
+
ActionMailer::Base.deliveries.length.should == 1
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe 'with a valid confirmation code' do
|
49
|
+
it "should confirm his account" do
|
50
|
+
record = base.described_class.find_and_confirm(@confirmable.confirmation_code)
|
51
|
+
record.errors.should be_empty
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should clean confirmation code" do
|
55
|
+
base.described_class.find_and_confirm(@confirmable.confirmation_code)
|
56
|
+
@confirmable.reload
|
57
|
+
@confirmable.confirmation_code.should be_nil
|
58
|
+
end
|
59
|
+
|
60
|
+
it "should set confirmed_at date" do
|
61
|
+
record = base.described_class.find_and_confirm(@confirmable.confirmation_code)
|
62
|
+
record.confirmed_at.should_not be_nil
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe 'with an invalid confirmation code' do
|
67
|
+
it "should set an error message" do
|
68
|
+
record = base.described_class.find_and_confirm('invalid_code')
|
69
|
+
record.errors.on(:confirmation_code).should == record.errors.generate_message(:confirmation_code, :invalid)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe 'when lost confirmation code' do
|
74
|
+
before(:each){ ActionMailer::Base.deliveries = [] }
|
75
|
+
|
76
|
+
it "should resend confirmation code if account is not confirmed" do
|
77
|
+
record = base.described_class.find_and_resend_confirmation_code(:email => @confirmable.email)
|
78
|
+
record.errors.should be_empty
|
79
|
+
ActionMailer::Base.deliveries.length.should == 1
|
80
|
+
end
|
81
|
+
|
82
|
+
it "should not resend confirmation code if account is confirmed" do
|
83
|
+
@confirmable.confirm!
|
84
|
+
record = base.described_class.find_and_resend_confirmation_code(:email => @confirmable.email)
|
85
|
+
record.errors.on(:email).should == record.errors.generate_message(:email, :already_confirmed)
|
86
|
+
ActionMailer::Base.deliveries.length.should == 0
|
87
|
+
end
|
88
|
+
|
89
|
+
it "should show a error message on resend confirmation code if e-mail is not valid" do
|
90
|
+
record = base.described_class.find_and_resend_confirmation_code(:email => 'invalid')
|
91
|
+
record.errors.on(:email).should == record.errors.generate_message(:email, :not_found)
|
92
|
+
ActionMailer::Base.deliveries.length.should == 0
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
|
3
|
+
module AuthHelpers
|
4
|
+
module Spec
|
5
|
+
module Notifier
|
6
|
+
def self.included(base)
|
7
|
+
base.class_eval do
|
8
|
+
before(:each) do
|
9
|
+
@member = OpenStruct.new(:email => 'recipient@email.com',
|
10
|
+
:confirmation_code => '0123456789',
|
11
|
+
:reset_password_code => 'abcdefghij')
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should deliver new account notification" do
|
15
|
+
email = ::AuthHelpers::Notifier.create_new_account(@member)
|
16
|
+
email.to.should == [ 'recipient@email.com' ]
|
17
|
+
email.body.should match(/#{@member.confirmation_code}/)
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should deliver email changed notification" do
|
21
|
+
email = ::AuthHelpers::Notifier.create_email_changed(@member)
|
22
|
+
email.to.should == [ 'recipient@email.com' ]
|
23
|
+
email.body.should match(/#{@member.confirmation_code}/)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should deliver reset password code" do
|
27
|
+
email = ::AuthHelpers::Notifier.create_reset_password(@member)
|
28
|
+
email.to.should == [ 'recipient@email.com' ]
|
29
|
+
email.body.should match(/#{@member.reset_password_code}/)
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should resend confirmation code" do
|
33
|
+
email = ::AuthHelpers::Notifier.create_confirmation_code(@member)
|
34
|
+
email.to.should == [ 'recipient@email.com' ]
|
35
|
+
email.body.should match(/#{@member.confirmation_code}/)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module AuthHelpers
|
2
|
+
module Spec
|
3
|
+
|
4
|
+
module Recoverable
|
5
|
+
def self.included(base)
|
6
|
+
base.class_eval do
|
7
|
+
describe 'when forgot password' do
|
8
|
+
before(:each) do
|
9
|
+
@recoverable = base.described_class.create!(@valid_attributes)
|
10
|
+
ActionMailer::Base.deliveries = []
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should send a reset password code to the user" do
|
14
|
+
record = base.described_class.find_and_send_reset_password_code(:email => @recoverable.email)
|
15
|
+
record.errors.should be_empty
|
16
|
+
record.reset_password_code.should_not be_blank
|
17
|
+
ActionMailer::Base.deliveries.length.should == 1
|
18
|
+
end
|
19
|
+
|
20
|
+
describe 'and reset password code is sent' do
|
21
|
+
before(:each) do
|
22
|
+
base.described_class.find_and_send_reset_password_code(:email => @recoverable.email)
|
23
|
+
@recoverable.reload
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should reset password if reset password code is valid" do
|
27
|
+
record = base.described_class.find_and_reset_password(:reset_password_code => @recoverable.reset_password_code, :password => '654321', :password_confirmation => '654321')
|
28
|
+
record.errors.should be_empty
|
29
|
+
|
30
|
+
record = base.described_class.find_and_authenticate(:email => @recoverable.email, :password => '654321')
|
31
|
+
record.errors.should be_empty
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should not reset password if the given reset password code is invalid" do
|
35
|
+
record = base.described_class.find_and_reset_password(:reset_password_code => 'invalid_pass_code', :password => '654321', :password_confirmation => '654321')
|
36
|
+
record.errors.on(:reset_password_code).should == record.errors.generate_message(:reset_password_code, :invalid)
|
37
|
+
record = base.described_class.find_and_authenticate(:email => @recoverable.email, :password => '654321')
|
38
|
+
record.errors.should_not be_empty
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should not reset password if password doesn't match confirmation" do
|
42
|
+
record = base.described_class.find_and_reset_password(:reset_password_code => @recoverable.reset_password_code, :password => '654321', :password_confirmation => '123456')
|
43
|
+
record.errors.on(:password).should == record.errors.generate_message(:password, :confirmation)
|
44
|
+
|
45
|
+
record = base.described_class.find_and_authenticate(:email => @recoverable.email, :password => '654321')
|
46
|
+
record.errors.should_not be_empty
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should clean reset_password_code when password is successfully reset" do
|
50
|
+
base.described_class.find_and_reset_password(:reset_password_code => @recoverable.reset_password_code, :password => '654321', :password_confirmation => '654321')
|
51
|
+
@recoverable.reload
|
52
|
+
@recoverable.reset_password_code.should be_nil
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module AuthHelpers
|
2
|
+
module Spec
|
3
|
+
|
4
|
+
module Rememberable
|
5
|
+
def self.included(base)
|
6
|
+
base.class_eval do
|
7
|
+
describe 'when authenticating' do
|
8
|
+
before(:each){ @rememberable = base.described_class.create!(@valid_attributes) }
|
9
|
+
|
10
|
+
it 'should set the remember me token' do
|
11
|
+
@rememberable.remember_me!
|
12
|
+
@rememberable.token.should_not be_nil
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'should set the remember me token creation date' do
|
16
|
+
@rememberable.remember_me!
|
17
|
+
@rememberable.token_expires_at.should_not be_nil
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'should forget the remember me token' do
|
21
|
+
@rememberable.remember_me!
|
22
|
+
@rememberable.forget_me!
|
23
|
+
@rememberable.token.should be_nil
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'should forget the remember me token creation date' do
|
27
|
+
@rememberable.remember_me!
|
28
|
+
@rememberable.forget_me!
|
29
|
+
@rememberable.token_expires_at.should be_nil
|
30
|
+
end
|
31
|
+
|
32
|
+
if base.described_class.ancestors.include?(::AuthHelpers::Model::Authenticable)
|
33
|
+
it 'should find, authenticate and set token if remember me is true' do
|
34
|
+
base.described_class.find_and_authenticate(:email => @valid_attributes[:email], :password => @valid_attributes[:password], :remember_me => "1")
|
35
|
+
@rememberable.reload
|
36
|
+
@rememberable.token.should_not be_nil
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'should find, authenticate and clear token if remember me is false' do
|
40
|
+
@rememberable.remember_me!
|
41
|
+
base.described_class.find_and_authenticate(:email => @valid_attributes[:email], :password => @valid_attributes[:password], :remember_me => "0")
|
42
|
+
@rememberable.reload
|
43
|
+
@rememberable.token.should be_nil
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'should not set or clear token if remember me is not set' do
|
47
|
+
@rememberable.remember_me!
|
48
|
+
base.described_class.find_and_authenticate(:email => @valid_attributes[:email], :password => @valid_attributes[:password])
|
49
|
+
@rememberable.reload
|
50
|
+
@rememberable.token.should_not be_nil
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'should not set or clear token if user cannot authenticate' do
|
54
|
+
base.described_class.find_and_authenticate(:email => @valid_attributes[:email], :password => @valid_attributes[:password].to_s.next, :remember_me => "1")
|
55
|
+
@rememberable.reload
|
56
|
+
@rememberable.token.should be_nil
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'should be found by remember me token' do
|
61
|
+
@rememberable.remember_me!
|
62
|
+
base.described_class.find_by_remember_me_token(@rememberable.token).should_not be_nil
|
63
|
+
base.described_class.find_by_remember_me_token(@rememberable.token.next).should be_nil
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'should not be found if remember me token is expired' do
|
67
|
+
@rememberable.remember_me!
|
68
|
+
@rememberable.update_attribute(:token_expires_at, 1.day.ago)
|
69
|
+
base.described_class.find_by_remember_me_token(@rememberable.token).should be_nil
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module AuthHelpers
|
2
|
+
module Spec
|
3
|
+
|
4
|
+
module Validatable
|
5
|
+
def self.included(base)
|
6
|
+
base.class_eval do
|
7
|
+
describe 'validation' do
|
8
|
+
should_validate_presence_of :email
|
9
|
+
should_validate_length_of :email, :within => 0..100, :allow_blank => true
|
10
|
+
should_validate_confirmation_of :email
|
11
|
+
|
12
|
+
it {
|
13
|
+
base.described_class.create!(@valid_attributes)
|
14
|
+
should validate_uniqueness_of(:email, :case_sensitive => false, :allow_blank => true,
|
15
|
+
:scope => (defined?(base.described_class::SCOPE) ? base.described_class::SCOPE : []))
|
16
|
+
}
|
17
|
+
|
18
|
+
should_not_allow_values_for :email, 'josevalim', 'a@a@a.com', 'jose@com'
|
19
|
+
|
20
|
+
should_validate_presence_of :password
|
21
|
+
should_validate_length_of :password, :within => 6..20, :allow_blank => true
|
22
|
+
should_validate_confirmation_of :password
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
data/lib/auth_helpers.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
module AuthHelpers
|
2
|
+
# Helper that find or initialize an object by attribute only if the given value is not blank.
|
3
|
+
# If it's blank, create a new object using :new.
|
4
|
+
#
|
5
|
+
def self.find_or_initialize_by_unless_blank(klass, attr, value)
|
6
|
+
if value.blank?
|
7
|
+
klass.new
|
8
|
+
else
|
9
|
+
klass.send(:"find_or_initialize_by_#{attr}", value)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# Helpers that generates a unique code for the given attribute by checking in
|
14
|
+
# the database if the code already exists.
|
15
|
+
#
|
16
|
+
def self.generate_unique_string_for(klass, attr, length=40)
|
17
|
+
begin
|
18
|
+
value = AuthHelpers.random_string(length)
|
19
|
+
end while klass.send(:"find_by_#{attr}", value)
|
20
|
+
|
21
|
+
value
|
22
|
+
end
|
23
|
+
|
24
|
+
# Create a random string with the given length using letters and numbers.
|
25
|
+
#
|
26
|
+
def self.random_string(length)
|
27
|
+
chars = ('a'..'z').to_a + ('A'..'Z').to_a + ('0'..'9').to_a
|
28
|
+
|
29
|
+
newpass = ''
|
30
|
+
1.upto(length) { |i| newpass << chars.rand }
|
31
|
+
|
32
|
+
return newpass
|
33
|
+
end
|
34
|
+
end
|
metadata
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: josevalim-auth_helpers
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- "Jos\xC3\xA9 Valim"
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-04-23 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: remarkable_rails
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 3.0.7
|
24
|
+
version:
|
25
|
+
description: AuthHelpers is a collection of modules to include in your model to deal with authentication.
|
26
|
+
email: jose.valim@gmail.com
|
27
|
+
executables: []
|
28
|
+
|
29
|
+
extensions: []
|
30
|
+
|
31
|
+
extra_rdoc_files:
|
32
|
+
- README
|
33
|
+
files:
|
34
|
+
- CHANGELOG
|
35
|
+
- MIT-LICENSE
|
36
|
+
- README
|
37
|
+
- init.rb
|
38
|
+
- lib/auth_helpers.rb
|
39
|
+
- lib/auth_helpers/migration.rb
|
40
|
+
- lib/auth_helpers/notifier.rb
|
41
|
+
- lib/auth_helpers/model/associatable.rb
|
42
|
+
- lib/auth_helpers/model/authenticable.rb
|
43
|
+
- lib/auth_helpers/model/confirmable.rb
|
44
|
+
- lib/auth_helpers/model/recoverable.rb
|
45
|
+
- lib/auth_helpers/model/rememberable.rb
|
46
|
+
- lib/auth_helpers/model/validatable.rb
|
47
|
+
- lib/auth_helpers/spec/associatable.rb
|
48
|
+
- lib/auth_helpers/spec/authenticable.rb
|
49
|
+
- lib/auth_helpers/spec/confirmable.rb
|
50
|
+
- lib/auth_helpers/spec/notifier.rb
|
51
|
+
- lib/auth_helpers/spec/recoverable.rb
|
52
|
+
- lib/auth_helpers/spec/rememberable.rb
|
53
|
+
- lib/auth_helpers/spec/validatable.rb
|
54
|
+
has_rdoc: true
|
55
|
+
homepage: http://github.com/josevalim/auth_helpers
|
56
|
+
post_install_message:
|
57
|
+
rdoc_options:
|
58
|
+
- --main
|
59
|
+
- README
|
60
|
+
require_paths:
|
61
|
+
- lib
|
62
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: "0"
|
67
|
+
version:
|
68
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: "0"
|
73
|
+
version:
|
74
|
+
requirements: []
|
75
|
+
|
76
|
+
rubyforge_project:
|
77
|
+
rubygems_version: 1.2.0
|
78
|
+
signing_key:
|
79
|
+
specification_version: 2
|
80
|
+
summary: AuthHelpers is a collection of modules to include in your model to deal with authentication.
|
81
|
+
test_files: []
|
82
|
+
|