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,231 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Negroni
4
+ # `Configuration` encapsulates all of the configuration for Negroni,
5
+ # keeping it out of the main module.
6
+ class Configuration
7
+ # The default email validation regex.
8
+ DEFAULT_EMAIL_VALIDATION_REGEX = /\A[^@\s]+@[^@\s]+\z/i
9
+ # /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
10
+
11
+ # @!group Authenticable Configuration
12
+
13
+ # Keys used when authenticating
14
+ # @return [Array<Symbol>]
15
+ attr_accessor :authentication_keys
16
+
17
+ # Keys that should be treated as case-insensitive
18
+ # @return [Array<Symbol>]
19
+ attr_accessor :case_insensitive_keys
20
+
21
+ # Keys that should have whitespace skipped
22
+ # @return [Array<Symbol>]
23
+ attr_accessor :strip_whitespace_keys
24
+
25
+ # When true, send an email to notify password changes
26
+ # @return [Boolean]
27
+ attr_accessor :send_password_change_notification
28
+
29
+ # How long before a token is expired. If nil is provided, token will
30
+ # last forever.
31
+ #
32
+ # @return [ActiveSupport::Duration]
33
+ attr_accessor :token_lifetime
34
+
35
+ # The audience claim to identify the recipients that the token
36
+ # is intended for.
37
+ #
38
+ # @return [Object]
39
+ attr_accessor :token_audience
40
+
41
+ # The name of the exception class that will be raised upon receiving an
42
+ # invalid auth token.
43
+ #
44
+ # Default: {Negroni::TokenNotFound}
45
+ #
46
+ # @return [Class, String]
47
+ attr_accessor :not_found_exception
48
+
49
+ # @!group Validatable Configuration
50
+
51
+ # Regular expression to validate emails
52
+ # @return [RegExp]
53
+ attr_accessor :email_regexp
54
+
55
+ # Range validation for password
56
+ # @return [Range]
57
+ attr_accessor :password_length
58
+
59
+ # @!group Encryption Configuration
60
+
61
+ # The number of times to hash the password
62
+ # @return [Integer]
63
+ attr_accessor :stretches
64
+
65
+ # Used to hash the password. Generate one with `rake secret`.
66
+ # @return [String] the pepper used to hash the password
67
+ attr_accessor :pepper
68
+
69
+ # The algorithm used to encode the token. Default: 'HS256'
70
+ #
71
+ # @return [String]
72
+ attr_accessor :token_algorithm
73
+
74
+ # The secret key that will be used for the token. Default:
75
+ # {Negroni::secret_key}.
76
+ #
77
+ # @return [String]
78
+ attr_accessor :token_secret
79
+
80
+ # An optional public key used to decode tokens.
81
+ #
82
+ # @return [String]
83
+ attr_accessor :token_public_key
84
+
85
+ # @!group Lockable Configuration
86
+
87
+ # Defines which strategy will be used to lock an account.
88
+ #
89
+ # * `:failed_attempts` = Locks an account after a number of failed
90
+ # attempts.
91
+ # * `:none` = No lock strategy. You should handle locking by
92
+ # yourself.
93
+ #
94
+ # @return [Symbol] the name of the lock strategy.
95
+ attr_accessor :lock_strategy
96
+
97
+ # Defines which key will be used when locking and unlocking an account
98
+ # @return [Array<Symbol>]
99
+ attr_accessor :unlock_keys
100
+
101
+ # Defines which strategy can be used to unlock an account.
102
+ # Valid values: `:email,` `:time,` `:both`
103
+ # @return [Symbol]
104
+ attr_accessor :unlock_strategy
105
+
106
+ # Number of authentication tries before locking an account
107
+ # @return [Integer]
108
+ attr_accessor :maximum_attempts
109
+
110
+ # Time interval to unlock the account if `:time` is defined as
111
+ # `unlock_strategy`.
112
+ #
113
+ # @return [ActiveSupport::Duration]
114
+ attr_accessor :unlock_in
115
+
116
+ # Defines which key will be used when recovering the password for an account
117
+ # @return [Array<Symbol>]
118
+ attr_accessor :reset_password_keys
119
+
120
+ # Time interval you can reset your password with a reset password key
121
+ # @return [ActiveSupport::Duration]
122
+ attr_accessor :reset_password_within
123
+
124
+ # @!group Mailer Configuration
125
+
126
+ # The sender for all mailers
127
+ # @return [String] the email address for the sender
128
+ attr_accessor :mailer_sender
129
+
130
+ # The class `Mailer` should inherit from (`'ActionMailer::Base'` by default)
131
+ # @return [String]
132
+ attr_accessor :parent_mailer
133
+
134
+ # @!group Controller Configuration
135
+
136
+ # The class `Negroni::BaseController` should inherit from.
137
+ #
138
+ # Default: ActionController::API
139
+ #
140
+ # @return [String, Class]
141
+ attr_accessor :parent_controller
142
+
143
+ # @!endgroup
144
+
145
+ # Create a new instance of `Configuration`, using default values for all
146
+ # attributes.
147
+ def initialize # rubocop:disable Metrics/AbcSize,MethodLength
148
+ @authentication_keys = [:email]
149
+ @case_insensitive_keys = [:email]
150
+ @strip_whitespace_keys = [:email]
151
+ @send_password_change_notification = false
152
+ @token_lifetime = 1.day
153
+ @token_algorithm = 'HS256'
154
+ @not_found_exception = 'Negroni::TokenNotFound'
155
+ @email_regexp = DEFAULT_EMAIL_VALIDATION_REGEX
156
+ @password_length = 8..72
157
+ @stretches = 11
158
+ @pepper = nil
159
+ @lock_strategy = :failed_attempts
160
+ @unlock_keys = [:email]
161
+ @unlock_strategy = :both
162
+ @maximum_attempts = 20
163
+ @unlock_in = 1.hour
164
+ @reset_password_keys = [:email]
165
+ @reset_password_within = 6.hours
166
+ @mailer_sender = nil
167
+ @parent_mailer = 'ActionMailer::Base'
168
+ @parent_controller = 'ActionController::API'
169
+ end
170
+
171
+ # `Delegation` adds methods to the including or extending class to delegate
172
+ # parameters to an instance of {Configuration}.
173
+ #
174
+ # Additionally, it provides a method `{#configuration}`, which is simply a
175
+ # lazily-instantiated instance of {Configuration}.
176
+ #
177
+ module Delegation
178
+ # Create delegation method for configuration
179
+ #
180
+ def self.config_delegator(*attrs)
181
+ attrs.each do |attribute|
182
+ class_eval <<-METHOD, __FILE__, __LINE__ + 1
183
+ def #{attribute}
184
+ configuration.#{attribute}
185
+ end
186
+
187
+ def #{attribute}=(new_value)
188
+ configuration.#{attribute} = new_value
189
+ end
190
+ METHOD
191
+ end
192
+ end
193
+
194
+ # The configuration object
195
+ # @return [Configuration]
196
+ def configuration
197
+ @configuration ||= Configuration.new
198
+ end
199
+
200
+ # @!macro [attach] cfg.delegator
201
+ # Delegates `$1` to #configuration.
202
+ #
203
+ # {render:Configuration#$1}
204
+ #
205
+ config_delegator :authentication_keys
206
+ config_delegator :case_insensitive_keys
207
+ config_delegator :strip_whitespace_keys
208
+ config_delegator :send_password_change_notification
209
+ config_delegator :token_lifetime
210
+ config_delegator :token_audience
211
+ config_delegator :not_found_exception
212
+ config_delegator :email_regexp
213
+ config_delegator :password_length
214
+ config_delegator :stretches
215
+ config_delegator :pepper
216
+ config_delegator :token_algorithm
217
+ config_delegator :token_secret
218
+ config_delegator :token_public_key
219
+ config_delegator :lock_strategy
220
+ config_delegator :unlock_keys
221
+ config_delegator :unlock_strategy
222
+ config_delegator :maximum_attempts
223
+ config_delegator :unlock_in
224
+ config_delegator :reset_password_keys
225
+ config_delegator :reset_password_within
226
+ config_delegator :mailer_sender
227
+ config_delegator :parent_mailer
228
+ config_delegator :parent_controller
229
+ end
230
+ end
231
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Negroni
4
+ module Controllers
5
+ # Helpers for controllers
6
+ module Helpers
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ prepend PrependedMethods
11
+ end
12
+
13
+ # Methods that are prepended into the class that includes {Helpers}
14
+ module PrependedMethods
15
+ extend ActiveSupport::Concern
16
+
17
+ included do
18
+ rescue_from Negroni.not_found_exception, with: :not_found
19
+ end
20
+
21
+ private
22
+
23
+ def not_found
24
+ head :not_found
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'knock/authenticable'
4
+
5
+ module Negroni
6
+ module Controllers
7
+ # TokenAuthenticable adds methods to the including controller class to
8
+ # enable authentication via json web tokens. Basically, it just includes
9
+ # {Knock::Authenticable} in the class.
10
+ #
11
+ # @see Knock::Authenticable
12
+ module TokenAuthenticable
13
+ extend ActiveSupport::Concern
14
+
15
+ included do
16
+ include Knock::Authenticable
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bcrypt'
4
+
5
+ module Negroni
6
+ # Handles password encryption and digestion
7
+ module Encryptor
8
+ class << self
9
+ # Digest a password
10
+ #
11
+ # @param klass [Class] the class for which to digest
12
+ # @param password [String] the password to digest
13
+ def digest(klass, password)
14
+ password = "#{password}#{klass.pepper}" if klass.pepper.present?
15
+
16
+ ::BCrypt::Password.create(password, cost: klass.stretches).to_s
17
+ end
18
+
19
+ # Compare two passwords
20
+ #
21
+ # @param klass [Class] the class for which to digest
22
+ # @param hashed_password [String] the hashed password to compare
23
+ # @param password [String] the password to compare with `hashed_password`
24
+ def compare(klass, hashed_password, password)
25
+ return false if hashed_password.blank?
26
+
27
+ bcrypt = ::BCrypt::Password.new(hashed_password)
28
+ password = "#{password}#{klass.pepper}" if klass.pepper.present?
29
+ password = ::BCrypt::Engine.hash_secret(password, bcrypt.salt)
30
+
31
+ Negroni.secure_compare(password, hashed_password)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Negroni
4
+ # The Rails engine
5
+ class Engine < ::Rails::Engine
6
+ isolate_namespace Negroni
7
+
8
+ config.generators do |g|
9
+ g.api_only = true
10
+ g.test_framework :rspec, fixture_replacement: :factory_girl
11
+ g.factory_girl dir: 'spec/factories'
12
+ end
13
+
14
+ config.negroni = Negroni
15
+
16
+ initializer 'negroni.omniauth', after: :load_config_initializers,
17
+ before: :build_middleware_stack do |app|
18
+ Negroni.omniauth_configs.each do |_provider, config|
19
+ app.middleware.use config.strategy_class, *config.args do |strategy|
20
+ config.strategy = strategy
21
+ end
22
+ end
23
+ end
24
+
25
+ initializer 'negroni.secret_key' do |app|
26
+ Negroni.secret_key ||= app.secrets.secret_key_base
27
+ Negroni.token_secret ||= app.secrets.secret_key_base
28
+
29
+ Negroni.token_generator ||=
30
+ if (secret_key = Negroni.secret_key)
31
+ Negroni::TokenGenerator.new(secret_key: secret_key)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Negroni
4
+ # Namespace for Mailer-related code
5
+ module Mailers
6
+ # Common helpers for mailers
7
+ module Helpers
8
+ extend ActiveSupport::Concern
9
+
10
+ protected
11
+
12
+ # @!attribute [r] resource
13
+ # @return [Object, #email] the resource object that we are mailing to
14
+ attr_reader :resource
15
+
16
+ # @!attribute [r] scope_name
17
+ # @return [String] the name of the record's class, underscored
18
+ attr_reader :scope_name
19
+
20
+ # Configure default email options
21
+ #
22
+ # @param record [Object] the object (record) that we are mailing to
23
+ # @param action [Symbol] the mail action to perform
24
+ # @param opts [Hash] a hash of options that will be sent to `headers_for`
25
+ #
26
+ def negroni_mail(record, action, opts = {}, &block)
27
+ initialize_from_record(record)
28
+ mail headers_for(action, opts), &block
29
+ end
30
+
31
+ # Initialize the including class with a record
32
+ #
33
+ # @param record [Object] the record to send the email to
34
+ #
35
+ def initialize_from_record(record)
36
+ @scope_name = record.class.to_s.underscore
37
+ @resource = instance_variable_set("@#{scope_name}", record)
38
+ end
39
+
40
+ # Generate a set of mail headers for a given action
41
+ #
42
+ # @param action [String, Symbol, #to_s] The mail action that will occur
43
+ # @param opts [Hash] a hash of additional options to merge with the
44
+ # headers
45
+ def headers_for(action, opts = {})
46
+ headers = {
47
+ subject: subject_for(action),
48
+ to: resource.email,
49
+ from: mailer_from,
50
+ reply_to: mailer_reply_to,
51
+ template_path: template_paths,
52
+ template_name: action
53
+ }.merge(opts)
54
+
55
+ @email = headers[:to]
56
+ headers
57
+ end
58
+
59
+ def mailer_reply_to
60
+ mailer_sender(:reply_to)
61
+ end
62
+
63
+ def mailer_from
64
+ mailer_sender(:from)
65
+ end
66
+
67
+ def mailer_sender(sender = :from)
68
+ default_sender = default_params[sender]
69
+
70
+ unless default_sender.present?
71
+ ms = Negroni.mailer_sender
72
+ return ms.is_a?(Proc) ? instance_eval(&ms) : ms
73
+ end
74
+
75
+ if default_sender.respond_to?(:to_proc)
76
+ instance_eval(&default_sender)
77
+ else
78
+ default_sender
79
+ end
80
+ end
81
+
82
+ def template_paths
83
+ _prefixes.dup
84
+ end
85
+
86
+ # Set up a subject doing an I18n lookup. At first, it attempts to set a
87
+ # subject based on the current mapping:
88
+ #
89
+ # en:
90
+ # negroni:
91
+ # mailer:
92
+ # confirmation_instructions:
93
+ # user_subject: '...'
94
+ #
95
+ # If one does not exist, it fallbacks to ActionMailer default:
96
+ #
97
+ # en:
98
+ # negroni:
99
+ # mailer:
100
+ # confirmation_instructions:
101
+ # subject: '...'
102
+ #
103
+ # @param key [Symbol, String] the key to lookup for I18n
104
+ # @return [String]
105
+ def subject_for(key)
106
+ I18n.t(:"#{@scope_name}_subject",
107
+ scope: [:negroni, :mailer, key],
108
+ default: [:subject, key.to_s.humanize])
109
+ end
110
+ end
111
+ end
112
+ end