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 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
@@ -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
+