negroni 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +20 -0
  3. data/README.md +59 -0
  4. data/Rakefile +64 -0
  5. data/app/mailers/negroni/mailer.rb +45 -0
  6. data/app/views/negroni/mailer/password_change.html.erb +5 -0
  7. data/app/views/negroni/mailer/reset_password_instructions.html.erb +8 -0
  8. data/app/views/negroni/mailer/unlock_instructions.html.erb +7 -0
  9. data/config/locales/en.yml +9 -0
  10. data/config/routes.rb +4 -0
  11. data/lib/negroni.rb +209 -0
  12. data/lib/negroni/configuration.rb +231 -0
  13. data/lib/negroni/controllers/helpers.rb +29 -0
  14. data/lib/negroni/controllers/token_authenticable.rb +20 -0
  15. data/lib/negroni/encryptor.rb +35 -0
  16. data/lib/negroni/engine.rb +35 -0
  17. data/lib/negroni/mailers/helpers.rb +112 -0
  18. data/lib/negroni/models.rb +138 -0
  19. data/lib/negroni/models/authenticable.rb +197 -0
  20. data/lib/negroni/models/base.rb +318 -0
  21. data/lib/negroni/models/lockable.rb +216 -0
  22. data/lib/negroni/models/omniauthable.rb +33 -0
  23. data/lib/negroni/models/recoverable.rb +204 -0
  24. data/lib/negroni/models/registerable.rb +14 -0
  25. data/lib/negroni/models/validatable.rb +63 -0
  26. data/lib/negroni/modules.rb +12 -0
  27. data/lib/negroni/omniauth.rb +25 -0
  28. data/lib/negroni/omniauth/config.rb +81 -0
  29. data/lib/negroni/orm/active_record.rb +7 -0
  30. data/lib/negroni/orm/mongoid.rb +6 -0
  31. data/lib/negroni/param_filter.rb +53 -0
  32. data/lib/negroni/resolver.rb +17 -0
  33. data/lib/negroni/token_generator.rb +58 -0
  34. data/lib/negroni/token_not_found.rb +13 -0
  35. data/lib/negroni/version.rb +6 -0
  36. data/lib/tasks/negroni_tasks.rake +5 -0
  37. metadata +169 -0
@@ -0,0 +1,216 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Negroni
4
+ module Models
5
+ # Handles blocking a user access after a certain number of attempts.
6
+ # Lockable accepts two different strategies to unlock a user after it's
7
+ # blocked: email and time. The former will send an email to the user when
8
+ # the lock happens, containing a link to unlock its account. The second will
9
+ # unlock the user automatically after some configured time (ie 2.hours).
10
+ # It's also possible to set up lockable to use both email and time
11
+ # strategies.
12
+ #
13
+ # ## Options
14
+ #
15
+ # Lockable adds the following options to `negroni`:
16
+ #
17
+ # * `maximum_attempts`: how many attempts should be accepted before
18
+ # blocking the user.
19
+ # * `lock_strategy`: lock the user account by :failed_attempts or :none.
20
+ # * `unlock_strategy`: unlock the user account by :time, :email, :both or
21
+ # :none.
22
+ # * `unlock_in`: the time you want to lock the user after to lock happens.
23
+ # Only available when unlock_strategy is :time or :both.
24
+ # * `unlock_keys`: the keys you want to use when locking and unlocking.
25
+ #
26
+ module Lockable
27
+ extend ActiveSupport::Concern
28
+
29
+ # @!method lock_strategy_enabled?(strategy)
30
+ # @return [Boolean] if the passed strategy is enabled
31
+ delegate :lock_strategy_enabled?,
32
+ :unlock_strategy_enabled?,
33
+ to: 'self.class'
34
+
35
+ # Required fields
36
+ def self.required_fields(klass)
37
+ att = []
38
+ att << :failed_attempts if klass.lock_strategy_enabled? :failed_attempts
39
+ att << :locked_at if klass.unlock_strategy_enabled? :time
40
+ att << :unlock_token if klass.unlock_strategy_enabled? :email
41
+ att
42
+ end
43
+
44
+ # Lock a user setting its locked_at to actual time.
45
+ #
46
+ # @param opts [Hash] hash of options
47
+ # @option opts [Boolean] :send_instructions pass `false` to not send the
48
+ # email
49
+ def lock_access!(opts = {})
50
+ self.locked_at = Time.now.utc
51
+
52
+ if unlock_strategy_enabled?(:email) &&
53
+ opts.fetch(:send_instructions, true)
54
+ send_unlock_instructions
55
+ else
56
+ save(validate: false)
57
+ end
58
+ end
59
+
60
+ # Unlock a user, clearing `locked_at` and `failed_attempts`.
61
+ def unlock_access!
62
+ self.locked_at = nil
63
+ self.failed_attempts = 0 if respond_to?(:failed_attempts=)
64
+ self.unlock_token = nil if respond_to?(:unlock_token=)
65
+ save(validate: false)
66
+ end
67
+
68
+ # Returns true if the user has access locked
69
+ # @return [Boolean]
70
+ def access_locked?
71
+ locked_at && !lock_expired?
72
+ end
73
+
74
+ # send instructions to unlock
75
+ def send_unlock_instructions
76
+ raw, enc = Negroni.token_generator.generate(self.class, :unlock_token)
77
+ self.unlock_token = enc
78
+
79
+ save(validate: false) # rubocop:disable Rails/SaveBang
80
+ send_auth_notification(:unlock_instructions, raw, {})
81
+ raw
82
+ end
83
+
84
+ # Resend instructions if the user is locked
85
+ def resend_unlock_instructions
86
+ if_access_locked { send_unlock_instructions }
87
+ end
88
+
89
+ # Override active_for_auth? for locking purposes
90
+ def active_for_auth?
91
+ super && !access_locked?
92
+ end
93
+
94
+ # Override for locking purposes
95
+ def inactive_message
96
+ access_locked? ? :locked : super
97
+ end
98
+
99
+ # rubocop:disable Metrics/CyclomaticComplexity,PerceivedComplexity
100
+
101
+ # Override for locking purposes
102
+ def valid_for_auth?
103
+ return super unless persisted? &&
104
+ lock_strategy_enabled?(:failed_attempts)
105
+
106
+ unlock_access! if lock_expired?
107
+
108
+ return true if super && !access_locked?
109
+
110
+ self.failed_attempts ||= 0
111
+ self.failed_attempts += 1
112
+
113
+ if attempts_exceeded?
114
+ lock_access! unless access_locked?
115
+ else
116
+ save(validate: false)
117
+ end
118
+
119
+ false
120
+ end
121
+
122
+ # Override for locking
123
+ def unauthenticated_message
124
+ if Negroni.paranoid
125
+ super
126
+ elsif access_locked? ||
127
+ (lock_strategy_enabled?(:failed_attempts) && attempts_exceeded?)
128
+ :locked
129
+ elsif lock_strategy_enabled?(:failed_attempts) &&
130
+ last_attempt? && self.class.last_attempt_warning
131
+ :last_attempt
132
+ else
133
+ super
134
+ end
135
+ end
136
+
137
+ protected
138
+
139
+ def attempts_exceeded?
140
+ failed_attempts >= self.class.maximum_attempts
141
+ end
142
+
143
+ def last_attempt?
144
+ failed_attempts == self.class.maximum_attempts - 1
145
+ end
146
+
147
+ def lock_expired?
148
+ return false unless unlock_strategy_enabled?(:time)
149
+ locked_at && locked_at < self.class.unlock_in.ago
150
+ end
151
+
152
+ def if_access_locked
153
+ if access_locked?
154
+ yield
155
+ else
156
+ errors.add(Negroni.unlock_keys.first, :not_locked)
157
+ false
158
+ end
159
+ end
160
+
161
+ # Class Methods for Lockable
162
+ module ClassMethods
163
+ # @private
164
+ BOTH_STRATEGIES = [:time, :email].freeze
165
+ private_constant :BOTH_STRATEGIES
166
+
167
+ Negroni::Models.config(self, :maximum_attempts, :lock_strategy,
168
+ :unlock_strategy, :unlock_in, :unlock_keys,
169
+ :last_attempt_warning)
170
+
171
+ # Attempt to find a user by its unlock keys. If a record is found, send
172
+ # new unlock instructions to it. If not user is found, returns a new
173
+ # user with an email not found error. Options must contain the user's
174
+ # unlock keys Checks if a given lock strategy is enabled.
175
+ def send_unlock_instructions(attributes = {})
176
+ lockable = find_or_initialize_with_errors(
177
+ unlock_keys, attributes, :not_found
178
+ )
179
+ lockable.resend_unlock_instructions if lockable.persisted?
180
+ lockable
181
+ end
182
+
183
+ # Find a user by its unlock token and try to unlock it.
184
+ # If no user is found, returns a new user with an error.
185
+ # If the user is not locked, creates an error for the user
186
+ # Options must have the unlock_token
187
+ def unlock_access_by_token(token)
188
+ orig = token
189
+ token = Negroni.token_generator.digest(self, :unlock_token, token)
190
+
191
+ lockable = find_or_initialize_with_error_by(:unlock_token, token)
192
+ lockable.unlock_access! if lockable.persisted?
193
+ lockable.unlock_token = orig
194
+ lockable
195
+ end
196
+
197
+ # @param strategy [Symbol] the name of the strategy.
198
+ #
199
+ # @return [Boolean] if the strategy is enabled.
200
+ def lock_strategy_enabled?(strategy)
201
+ lock_strategy == strategy
202
+ end
203
+
204
+ # Checks if a given unlock strategy is enabled.
205
+ #
206
+ # @param strategy [Symbol] the name of the strategy.
207
+ #
208
+ # @return [Boolean] if the strategy is enabled.
209
+ def unlock_strategy_enabled?(strategy)
210
+ unlock_strategy == strategy ||
211
+ (unlock_strategy == :both && BOTH_STRATEGIES.include?(strategy))
212
+ end
213
+ end
214
+ end
215
+ end
216
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'negroni/omniauth'
4
+
5
+ module Negroni
6
+ module Models
7
+ # Adds OmniAuth support to your model.
8
+ #
9
+ # ## Options
10
+ #
11
+ # Omniauthable adds the following options to negroni:
12
+ #
13
+ # * `omniauth_providers`: Which providers are available to this model.
14
+ # It expects an array.
15
+ #
16
+ # @example
17
+ # negroni :authenticable, :omniauthable, omniauth_providers: [:twitter]
18
+ #
19
+ module Omniauthable
20
+ extend ActiveSupport::Concern
21
+
22
+ # Required fields for this module (_none_)
23
+ def self.required_fields(_klass = nil)
24
+ []
25
+ end
26
+
27
+ # Configuration options
28
+ module ClassMethods
29
+ Negroni::Models.config(self, :omniauth_providers)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,204 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Negroni
4
+ module Models
5
+ # Recoverable takes care of resetting the user password and send reset
6
+ # instructions.
7
+ #
8
+ # ## Required attributes
9
+ #
10
+ # Recoverable requires that the including class has the following attributes:
11
+ #
12
+ # * `reset_password_sent_at`: [DateTime]
13
+ # * `reset_password_token`: [String]
14
+ #
15
+ # ## Options
16
+ #
17
+ # Recoverable adds the following class options:
18
+ #
19
+ # * `reset_password_keys`: the keys you want to use when recovering the
20
+ # password for an account
21
+ # * `reset_password_within`: the time period within which the password must
22
+ # be reset or the token expires.
23
+ # * `sign_in_after_reset_password`: whether or not to sign in the user
24
+ # automatically after a password reset.
25
+ #
26
+ # @example
27
+ # # resets the user password and save the record, true if valid passwords
28
+ # # are given, otherwise false
29
+ # User.find(1).reset_password('password123', 'password123')
30
+ #
31
+ # @example
32
+ # # creates a new token and send it with instructions about how to reset
33
+ # # the password
34
+ # User.find(1).send_reset_password_instructions
35
+ #
36
+ module Recoverable
37
+ extend ActiveSupport::Concern
38
+
39
+ # Required fields for Recoverable
40
+ def self.required_fields(_klass = nil)
41
+ [:reset_password_sent_at, :reset_password_token]
42
+ end
43
+
44
+ included do
45
+ before_update :clear_reset_password_token,
46
+ if: :clear_reset_password_token?
47
+ end
48
+
49
+ # Update password saving the record and clearing token. Returns true if
50
+ # the passwords are valid and the record was saved, false otherwise.
51
+ #
52
+ # @param new_password [String] the new password
53
+ # @param new_password_confirmation [String] confirmation of the password
54
+ #
55
+ # @return [Boolean] if the operation was successful
56
+ def reset_password(new_password, new_password_confirmation)
57
+ self.password = new_password
58
+ self.password_confirmation = new_password_confirmation
59
+
60
+ save
61
+ end
62
+
63
+ # Resets reset password token and send reset password instructions by
64
+ # email. Returns the token sent in the e-mail.
65
+ #
66
+ # @return [String] the token that was sent in the email.
67
+ def send_reset_password_instructions
68
+ token = set_reset_password_token
69
+ send_reset_password_instructions_notification(token)
70
+
71
+ token
72
+ end
73
+
74
+ # Checks if the reset password token sent is within the limit time. We do
75
+ # this by calculating if the difference between today and the sending date
76
+ # does not exceed the confirm in time configured. Returns true if the
77
+ # resource is not responding to reset_password_sent_at at all.
78
+ #
79
+ # @return [Boolean] if the period is valid.
80
+ #
81
+ # reset_password_within is a model configuration, must always be an integer
82
+ # value.
83
+ #
84
+ # @example
85
+ # # reset_password_within = 1.day and reset_password_sent_at = today
86
+ # reset_password_period_valid? # => true
87
+ #
88
+ # @example
89
+ # # reset_password_within = 5.days; reset_password_sent_at = 4.days.ago
90
+ # reset_password_period_valid? # => true
91
+ #
92
+ # @example
93
+ # # reset_password_within = 5.days; reset_password_sent_at = 5.days.ago
94
+ # reset_password_period_valid? # => false
95
+ #
96
+ # @example
97
+ # # reset_password_within = 0.days
98
+ # reset_password_period_valid? # will always return false
99
+ #
100
+ def reset_password_period_valid?
101
+ reset_password_sent_at &&
102
+ reset_password_sent_at.utc >= self.class.reset_password_within.ago.utc
103
+ end
104
+
105
+ protected
106
+
107
+ # Removes `reset_password_token`
108
+ def clear_reset_password_token
109
+ self.reset_password_token = nil
110
+ self.reset_password_sent_at = nil
111
+ end
112
+
113
+ # Sets the `reset_password_token` for the record
114
+ def set_reset_password_token
115
+ raw, encoded = Negroni.token_generator.generate(self.class,
116
+ :reset_password_token)
117
+
118
+ self.reset_password_token = encoded
119
+ self.reset_password_sent_at = Time.now.utc
120
+
121
+ save(validate: false) # rubocop:disable Rails/SaveBang
122
+ raw
123
+ end
124
+
125
+ def send_reset_password_instructions_notification(token)
126
+ send_auth_notification(:reset_password_instructions, token, {})
127
+ end
128
+
129
+ def clear_reset_password_token?
130
+ auth_keys_changed = self.class.authentication_keys.any? do |attribute|
131
+ respond_to?("#{attribute}_changed?") && send("#{attribute}_changed?")
132
+ end
133
+
134
+ !new_record? && (auth_keys_changed || password_digest_changed?)
135
+ end
136
+
137
+ # Class Methods for `Recoverable`
138
+ module ClassMethods
139
+ # Attempt to find a user by password reset token. If a user is found,
140
+ # return it. If not, return `nil`.
141
+ def with_reset_password_token(token)
142
+ reset_password_token = Negroni.token_generator.digest(
143
+ self, :reset_password_token, token
144
+ )
145
+ to_adapter.find_first(reset_password_token: reset_password_token)
146
+ end
147
+
148
+ # Attempt to find a user by its email. If a record is found, send new
149
+ # password instructions to it. If user is not found, returns a new user
150
+ # with an email not found error.
151
+ #
152
+ # Attributes must contain the user's email
153
+ def send_reset_password_instructions(attributes = {})
154
+ recoverable = find_or_initialize_with_errors(reset_password_keys,
155
+ attributes,
156
+ :not_found)
157
+
158
+ recoverable.send_reset_password_instructions if recoverable.persisted?
159
+ recoverable
160
+ end
161
+
162
+ # rubocop:disable Metrics/MethodLength
163
+
164
+ # Attempt to find a user by its reset_password_token to reset its
165
+ # password. If a user is found and token is still valid, reset its
166
+ # password and automatically try saving the record. If not user is
167
+ # found, returns a new user containing an error in reset_password_token
168
+ # attribute.
169
+ #
170
+ # Attributes must contain reset_password_token, password and confirmation
171
+ def reset_password_by_token(attributes = {})
172
+ original = attributes[:reset_password_token]
173
+ token = Negroni.token_generator.digest(
174
+ self, :reset_password_token, original
175
+ )
176
+
177
+ recoverable = find_or_initialize_with_error_by(:reset_password_token,
178
+ token)
179
+
180
+ if recoverable.persisted?
181
+ if recoverable.reset_password_period_valid?
182
+ recoverable.reset_password(attributes[:password],
183
+ attributes[:password_confirmation])
184
+ else
185
+ recoverable.errors.add(:reset_password_token, :expired)
186
+ end
187
+ end
188
+
189
+ if recoverable.reset_password_token.present?
190
+ recoverable.reset_password_token = original
191
+ end
192
+
193
+ recoverable
194
+ end
195
+
196
+ Negroni::Models.config(self,
197
+ :reset_password_keys,
198
+ :reset_password_within)
199
+ end
200
+ end
201
+ end
202
+ end
203
+
204
+ # rubocop:enable all