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.
- checksums.yaml +7 -0
- data/LICENSE +20 -0
- data/README.md +59 -0
- data/Rakefile +64 -0
- data/app/mailers/negroni/mailer.rb +45 -0
- data/app/views/negroni/mailer/password_change.html.erb +5 -0
- data/app/views/negroni/mailer/reset_password_instructions.html.erb +8 -0
- data/app/views/negroni/mailer/unlock_instructions.html.erb +7 -0
- data/config/locales/en.yml +9 -0
- data/config/routes.rb +4 -0
- data/lib/negroni.rb +209 -0
- data/lib/negroni/configuration.rb +231 -0
- data/lib/negroni/controllers/helpers.rb +29 -0
- data/lib/negroni/controllers/token_authenticable.rb +20 -0
- data/lib/negroni/encryptor.rb +35 -0
- data/lib/negroni/engine.rb +35 -0
- data/lib/negroni/mailers/helpers.rb +112 -0
- data/lib/negroni/models.rb +138 -0
- data/lib/negroni/models/authenticable.rb +197 -0
- data/lib/negroni/models/base.rb +318 -0
- data/lib/negroni/models/lockable.rb +216 -0
- data/lib/negroni/models/omniauthable.rb +33 -0
- data/lib/negroni/models/recoverable.rb +204 -0
- data/lib/negroni/models/registerable.rb +14 -0
- data/lib/negroni/models/validatable.rb +63 -0
- data/lib/negroni/modules.rb +12 -0
- data/lib/negroni/omniauth.rb +25 -0
- data/lib/negroni/omniauth/config.rb +81 -0
- data/lib/negroni/orm/active_record.rb +7 -0
- data/lib/negroni/orm/mongoid.rb +6 -0
- data/lib/negroni/param_filter.rb +53 -0
- data/lib/negroni/resolver.rb +17 -0
- data/lib/negroni/token_generator.rb +58 -0
- data/lib/negroni/token_not_found.rb +13 -0
- data/lib/negroni/version.rb +6 -0
- data/lib/tasks/negroni_tasks.rake +5 -0
- 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
|