negroni 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.
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