negroni-lite 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 +63 -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