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,138 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Negroni
|
4
|
+
# Namespace for Model-specific configuration.
|
5
|
+
module Models
|
6
|
+
extend ActiveSupport::Autoload
|
7
|
+
|
8
|
+
# Raised if a required attribute is missing.
|
9
|
+
class MissingAttribute < StandardError
|
10
|
+
# Create a MissingAttribute error with a list of attributes
|
11
|
+
def initialize(attributes)
|
12
|
+
@attributes = attributes
|
13
|
+
super(message)
|
14
|
+
end
|
15
|
+
|
16
|
+
# The error message
|
17
|
+
def message
|
18
|
+
verb, s = @attributes.count > 1 ? %w(are s) : ['is']
|
19
|
+
list = @attributes.join(', ')
|
20
|
+
|
21
|
+
"The following attribute#{s} #{verb} missing on your model: #{list}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Creates configuration values for Negroni and for the given module.
|
26
|
+
#
|
27
|
+
# Negroni::Models.config(Negroni::Authenticatable, :authentication_keys)
|
28
|
+
#
|
29
|
+
# The line above creates:
|
30
|
+
#
|
31
|
+
# 1) An accessor called Negroni.authentication_keys, which value is used
|
32
|
+
# by default;
|
33
|
+
#
|
34
|
+
# 2) Some class methods for your model Model.authentication_keys and
|
35
|
+
# Model.authentication_keys= which have higher priority than
|
36
|
+
# Negroni.authentication_keys;
|
37
|
+
#
|
38
|
+
# 3) And an instance method authentication_keys.
|
39
|
+
#
|
40
|
+
# To add the class methods you need to have a module ClassMethods defined
|
41
|
+
# inside the given class.
|
42
|
+
#
|
43
|
+
# @api private
|
44
|
+
def self.config(mod, *accessors) # rubocop:disable Metrics/MethodLength
|
45
|
+
class << mod; attr_accessor :available_configs; end
|
46
|
+
mod.available_configs = accessors
|
47
|
+
|
48
|
+
accessors.each do |accessor|
|
49
|
+
mod.class_eval <<-METHOD, __FILE__, __LINE__ + 1
|
50
|
+
def #{accessor}
|
51
|
+
if defined?(@#{accessor})
|
52
|
+
@#{accessor}
|
53
|
+
elsif superclass.respond_to?(:#{accessor})
|
54
|
+
superclass.#{accessor}
|
55
|
+
else
|
56
|
+
Negroni.#{accessor}
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def #{accessor}=(value)
|
61
|
+
@#{accessor} = value
|
62
|
+
end
|
63
|
+
METHOD
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Checks that the including class has the appropriate required fields.
|
68
|
+
#
|
69
|
+
# @param klass [Class] the including class
|
70
|
+
# @raise [Negroni::Models::MissingAttribute] if the check fails.
|
71
|
+
# @return [Void]
|
72
|
+
def self.check_fields!(klass)
|
73
|
+
failed_attributes = []
|
74
|
+
instance = klass.new
|
75
|
+
|
76
|
+
klass.negroni_modules.each do |mod|
|
77
|
+
constant = const_get(mod.to_s.classify)
|
78
|
+
|
79
|
+
constant.required_fields(klass).each do |field|
|
80
|
+
failed_attributes << field unless instance.respond_to?(field)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
return unless failed_attributes.any?
|
85
|
+
raise Negroni::Models::MissingAttribute, failed_attributes
|
86
|
+
end
|
87
|
+
|
88
|
+
# rubocop:disable Metrics/AbcSize,MethodLength
|
89
|
+
|
90
|
+
# Include the given Negroni modules in your model. Authenticate is always
|
91
|
+
# included.
|
92
|
+
#
|
93
|
+
# @param modules [Symbol, Array<Symbol>] the modules to include.
|
94
|
+
# @return [Void]
|
95
|
+
def negroni(*modules)
|
96
|
+
options = modules.extract_options!.dup
|
97
|
+
|
98
|
+
selected_modules = modules.map(&:to_sym).uniq.sort_by do |sym|
|
99
|
+
Negroni::ALL.index(sym) != -1
|
100
|
+
end
|
101
|
+
|
102
|
+
aa_module_hook! do
|
103
|
+
include Negroni::Models::Base
|
104
|
+
|
105
|
+
selected_modules.each do |m|
|
106
|
+
mod = Negroni::Models.const_get m.to_s.classify
|
107
|
+
|
108
|
+
if mod.const_defined? 'ClassMethods'
|
109
|
+
class_mod = mod.const_get('ClassMethods')
|
110
|
+
extend class_mod
|
111
|
+
|
112
|
+
if class_mod.respond_to? :available_configs
|
113
|
+
available_configs = class_mod.available_configs
|
114
|
+
|
115
|
+
available_configs.each do |config|
|
116
|
+
next unless options.key? config
|
117
|
+
send(:"#{config}=", options.delete(config))
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
include mod
|
123
|
+
end
|
124
|
+
|
125
|
+
self.negroni_modules |= selected_modules
|
126
|
+
options.each { |key, value| send(:"#{key}=", value) }
|
127
|
+
end
|
128
|
+
end
|
129
|
+
# rubocop:enable all
|
130
|
+
|
131
|
+
# a hook
|
132
|
+
def aa_module_hook!
|
133
|
+
yield
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
require 'negroni/models/base'
|
@@ -0,0 +1,197 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/concern'
|
4
|
+
|
5
|
+
module Negroni
|
6
|
+
module Models
|
7
|
+
# The `Authenticable` module should be included in any application classes
|
8
|
+
# that should be authenticable via a JSON web token.
|
9
|
+
#
|
10
|
+
# This module makes a few assumptions about your class:
|
11
|
+
# * It has an `email` attribute
|
12
|
+
#
|
13
|
+
module Authenticable
|
14
|
+
extend ActiveSupport::Concern
|
15
|
+
|
16
|
+
included do
|
17
|
+
after_update :send_password_change_notification,
|
18
|
+
if: :send_password_change_notification?
|
19
|
+
|
20
|
+
attr_reader :password, :current_password
|
21
|
+
|
22
|
+
attr_accessor :password_confirmation
|
23
|
+
end
|
24
|
+
|
25
|
+
# Required fields for this module
|
26
|
+
def self.required_fields(klass)
|
27
|
+
[:password_digest] + klass.authentication_keys
|
28
|
+
end
|
29
|
+
|
30
|
+
# Generates a hashed password based on the given value.
|
31
|
+
def password=(new_password)
|
32
|
+
@password = new_password
|
33
|
+
self.password_digest = digest_password(@password) if @password.present?
|
34
|
+
end
|
35
|
+
|
36
|
+
# Checks if a password is valid for the given instance
|
37
|
+
def valid_password?(password)
|
38
|
+
Negroni::Encryptor.compare(self.class, password_digest, password)
|
39
|
+
end
|
40
|
+
|
41
|
+
# @!group Updating a Record
|
42
|
+
|
43
|
+
# Update record attributes when :current_password matches, otherwise
|
44
|
+
# returns error on :current_password.
|
45
|
+
#
|
46
|
+
# This method also rejects the password field if it is blank (allowing
|
47
|
+
# users to change relevant information like the e-mail without changing
|
48
|
+
# their password). In case the password field is rejected, the confirmation
|
49
|
+
# is also rejected as long as it is also blank.
|
50
|
+
#
|
51
|
+
# @param params [Hash] params from the controller
|
52
|
+
# @param options [Hash] a hash of options
|
53
|
+
def update_with_password(params, *options)
|
54
|
+
current_password = params.delete :current_password
|
55
|
+
|
56
|
+
params = _sanitize_password_params(params)
|
57
|
+
|
58
|
+
result = if valid_password?(current_password)
|
59
|
+
update_attributes(params, *options)
|
60
|
+
else
|
61
|
+
_invalid_update(current_password, params, *options)
|
62
|
+
end
|
63
|
+
|
64
|
+
clean_up_passwords
|
65
|
+
result
|
66
|
+
end
|
67
|
+
|
68
|
+
# Updates record attributes without asking for the current password.
|
69
|
+
# Never allows a change to the current password. If you are using this
|
70
|
+
# method, you should probably override this method to protect other
|
71
|
+
# attributes you would not like to be updated without a password.
|
72
|
+
#
|
73
|
+
# @example
|
74
|
+
#
|
75
|
+
# def update_without_password(params, *options)
|
76
|
+
# params.delete(:email)
|
77
|
+
# super(params)
|
78
|
+
# end
|
79
|
+
#
|
80
|
+
def update_without_password(params, *options)
|
81
|
+
params.delete(:password)
|
82
|
+
params.delete(:password_confirmation)
|
83
|
+
|
84
|
+
result = update_attributes(params, *options)
|
85
|
+
clean_up_passwords
|
86
|
+
result
|
87
|
+
end
|
88
|
+
|
89
|
+
# Destroy record when :current_password matches, otherwise returns
|
90
|
+
# error on :current_password. It also automatically rejects
|
91
|
+
# :current_password if it is blank.
|
92
|
+
#
|
93
|
+
# @param current_password [String] the current password for the record
|
94
|
+
#
|
95
|
+
def destroy_with_password(current_password)
|
96
|
+
result = if valid_password?(current_password)
|
97
|
+
destroy # rubocop:disable Rails/SaveBang
|
98
|
+
else
|
99
|
+
valid?
|
100
|
+
message = current_password.blank? ? :blank : :invalid
|
101
|
+
errors.add(:current_password, message)
|
102
|
+
false
|
103
|
+
end
|
104
|
+
|
105
|
+
result
|
106
|
+
end
|
107
|
+
|
108
|
+
# @!group Authentication Methods
|
109
|
+
|
110
|
+
# Authenticates the including class with `unencrypted_password`.
|
111
|
+
#
|
112
|
+
# @param unencrypted_password [String] the password to auth against
|
113
|
+
#
|
114
|
+
# @return [Boolean] if the user is successfully authenticated
|
115
|
+
def authenticate(unencrypted_password)
|
116
|
+
valid_password?(unencrypted_password) && self
|
117
|
+
end
|
118
|
+
|
119
|
+
# Authenticates the including class with `unencrypted_password`.
|
120
|
+
#
|
121
|
+
# @param unencrypted_password [String] the password to auth against
|
122
|
+
#
|
123
|
+
# @raise [ActiveRecord::RecordNotFound] if the user is not successfully
|
124
|
+
# authenticated
|
125
|
+
#
|
126
|
+
# @return [Boolean] if the user is successfully authenticated
|
127
|
+
def authenticate!(unencrypted_password)
|
128
|
+
authenticate(unencrypted_password) || raise('Bad password!')
|
129
|
+
end
|
130
|
+
|
131
|
+
# @!endgroup
|
132
|
+
|
133
|
+
# Reliably returns the salt, regardless of implementation
|
134
|
+
#
|
135
|
+
# @return [String]
|
136
|
+
def authenticable_salt
|
137
|
+
password_digest[0, 29] if password_digest
|
138
|
+
end
|
139
|
+
|
140
|
+
protected
|
141
|
+
|
142
|
+
def digest_password(password)
|
143
|
+
Negroni::Encryptor.digest(self.class, password)
|
144
|
+
end
|
145
|
+
|
146
|
+
def send_password_change_notification?
|
147
|
+
self.class.send_password_change_notification && password_digest_changed?
|
148
|
+
end
|
149
|
+
|
150
|
+
# @!group Callbacks
|
151
|
+
|
152
|
+
def send_password_change_notification
|
153
|
+
send_auth_notification(:password_change)
|
154
|
+
end
|
155
|
+
|
156
|
+
# Cleans up the @password and @password_confirmation ivars
|
157
|
+
def clean_up_passwords
|
158
|
+
self.password = self.password_confirmation = nil
|
159
|
+
end
|
160
|
+
|
161
|
+
# @!endgroup Callbacks
|
162
|
+
|
163
|
+
private
|
164
|
+
|
165
|
+
# remove :password and :password_confirmation if they are present
|
166
|
+
#
|
167
|
+
# @param params [Hash] the params hash to sanitize
|
168
|
+
# @return [Hash] the sanitized params
|
169
|
+
def _sanitize_password_params(params, remove_all: false)
|
170
|
+
params.delete(:password) if params[:password].blank? || remove_all
|
171
|
+
|
172
|
+
if params[:password_confirmation].blank? || remove_all
|
173
|
+
params.delete(:password_confirmation)
|
174
|
+
end
|
175
|
+
|
176
|
+
params
|
177
|
+
end
|
178
|
+
|
179
|
+
def _invalid_update(current_password, params, *options)
|
180
|
+
_sanitize_password_params(params, remove_all: true)
|
181
|
+
assign_attributes(params, *options)
|
182
|
+
valid?
|
183
|
+
|
184
|
+
message = current_password.blank? ? :blank : :invalid
|
185
|
+
errors.add(:current_password, message)
|
186
|
+
false
|
187
|
+
end
|
188
|
+
|
189
|
+
# Class methods for the Authenticate module.
|
190
|
+
module ClassMethods
|
191
|
+
Negroni::Models.config(
|
192
|
+
self, :pepper, :stretches, :send_password_change_notification
|
193
|
+
)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
@@ -0,0 +1,318 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Negroni
|
4
|
+
module Models
|
5
|
+
# The `Base` module contains methods that should be included in all Negroni
|
6
|
+
# models. It holds common settings for all authentication, as well as some
|
7
|
+
# shared behavior across all modules.
|
8
|
+
#
|
9
|
+
# ## Options
|
10
|
+
#
|
11
|
+
# Base adds the following options:
|
12
|
+
#
|
13
|
+
# * `authentication_keys`: parameters used for authentication.
|
14
|
+
# Default [:email].
|
15
|
+
#
|
16
|
+
# * `strip_whitespace_keys`: keys from which whitespace should be
|
17
|
+
# stripped prior to validation. Default [:email].
|
18
|
+
#
|
19
|
+
module Base
|
20
|
+
extend ActiveSupport::Concern
|
21
|
+
|
22
|
+
# Blacklisted attributes for serialization.
|
23
|
+
SERIALIZATION_BLACKLIST = [
|
24
|
+
:password_digest, :reset_password_token, :reset_password_sent_at,
|
25
|
+
:password_salt, :confirmation_token, :confirmed_at,
|
26
|
+
:confirmation_sent_at, :failed_attempts, :unlock_token, :locked_at
|
27
|
+
].freeze
|
28
|
+
|
29
|
+
included do
|
30
|
+
class_attribute :negroni_modules, instance_writer: false
|
31
|
+
self.negroni_modules ||= []
|
32
|
+
|
33
|
+
before_validation :downcase_keys
|
34
|
+
before_validation :strip_whitespace
|
35
|
+
end
|
36
|
+
|
37
|
+
# Required fields for this module (_none_).
|
38
|
+
def self.required_fields(_klass)
|
39
|
+
[]
|
40
|
+
end
|
41
|
+
|
42
|
+
# Returns whether or not the including class is active for
|
43
|
+
# authentication. This method is primarily intended to be overriden by
|
44
|
+
# other modules, including `Lockable`.
|
45
|
+
#
|
46
|
+
# Yields and returns the return value of the given block, if a block is
|
47
|
+
# given. Otherwise returns true.
|
48
|
+
#
|
49
|
+
# @return [Boolean]
|
50
|
+
def valid_for_auth?
|
51
|
+
block_given? ? yield : true
|
52
|
+
end
|
53
|
+
|
54
|
+
# The message that will be populated if an invalid record attempts to
|
55
|
+
# authenticate.
|
56
|
+
#
|
57
|
+
# @return [Symbol] the key of the translation to look up
|
58
|
+
def unauthenticated_message
|
59
|
+
:invalid
|
60
|
+
end
|
61
|
+
|
62
|
+
# Returns whether or not the including class is active for
|
63
|
+
# authentication. This method is primarily intended to be overriden by
|
64
|
+
# other modules, including `Lockable`.
|
65
|
+
#
|
66
|
+
# @return [Boolean]
|
67
|
+
def active_for_auth?
|
68
|
+
true
|
69
|
+
end
|
70
|
+
|
71
|
+
# The message that will be populated if an inactive record attempts to
|
72
|
+
# authenticate.
|
73
|
+
#
|
74
|
+
# @return [Symbol] the key of the translation to look up
|
75
|
+
def inactive_message
|
76
|
+
:inactive
|
77
|
+
end
|
78
|
+
|
79
|
+
# Redefine serializable_hash in models for more secure defaults. By
|
80
|
+
# default, it removes from the serializable model all attributes that are
|
81
|
+
# __not__ accessible. You can remove this default by using :force_except
|
82
|
+
# and passing a new list of attributes you want to exempt. All attributes
|
83
|
+
# given to :except will simply add names to exempt to Negroni internal
|
84
|
+
# list.
|
85
|
+
def serializable_hash(options = nil)
|
86
|
+
options ||= {}
|
87
|
+
options[:except] = Array(options[:except])
|
88
|
+
|
89
|
+
if options[:force_except]
|
90
|
+
options[:except].concat Array(options[:force_except])
|
91
|
+
else
|
92
|
+
options[:except].concat SERIALIZATION_BLACKLIST
|
93
|
+
end
|
94
|
+
|
95
|
+
super(options)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Redefine inspect using serializable_hash, to ensure we don't accidentally
|
99
|
+
# leak passwords into exceptions.
|
100
|
+
def inspect
|
101
|
+
inspection = serializable_hash.collect do |k, v|
|
102
|
+
value = if respond_to?(:attribute_for_inspect)
|
103
|
+
attribute_for_inspect(k)
|
104
|
+
else
|
105
|
+
v.inspect
|
106
|
+
end
|
107
|
+
|
108
|
+
"#{k}: #{value}"
|
109
|
+
end
|
110
|
+
|
111
|
+
"#<#{self.class} #{inspection.join(', ')}>"
|
112
|
+
end
|
113
|
+
|
114
|
+
protected
|
115
|
+
|
116
|
+
def auth_mailer
|
117
|
+
Negroni.mailer
|
118
|
+
end
|
119
|
+
|
120
|
+
# This is an internal method called every time Negroni needs to send a
|
121
|
+
# notification/mail. This can be overridden if you need to customize the
|
122
|
+
# e-mail delivery logic. For instance, if you are using a queue to deliver
|
123
|
+
# e-mails (delayed job, sidekiq, resque, etc), you must add the delivery
|
124
|
+
# to the queue just after the transaction was committed. To achieve this,
|
125
|
+
# you can override send_auth_notification to store the deliveries
|
126
|
+
# until the after_commit callback is triggered:
|
127
|
+
#
|
128
|
+
# @example
|
129
|
+
# class User
|
130
|
+
# negroni :authenticable, :confirmable
|
131
|
+
#
|
132
|
+
# after_commit :send_pending_notifications
|
133
|
+
#
|
134
|
+
# protected
|
135
|
+
#
|
136
|
+
# def send_auth_notification(notification, *args)
|
137
|
+
# # If the record is new or changed then delay the
|
138
|
+
# # delivery until the after_commit callback otherwise
|
139
|
+
# # send now because after_commit will not be called.
|
140
|
+
# if new_record? || changed?
|
141
|
+
# pending_notifications << [notification, args]
|
142
|
+
# else
|
143
|
+
# message = auth_mailer.send(notification, self, *args)
|
144
|
+
# Remove once we move to Rails 4.2+ only.
|
145
|
+
# if message.respond_to?(:deliver_now)
|
146
|
+
# message.deliver_now
|
147
|
+
# else
|
148
|
+
# message.deliver
|
149
|
+
# end
|
150
|
+
# end
|
151
|
+
# end
|
152
|
+
#
|
153
|
+
# def send_pending_notifications
|
154
|
+
# pending_notifications.each do |notification, args|
|
155
|
+
# message = auth_mailer.send(notification, self, *args)
|
156
|
+
# Remove once we move to Rails 4.2+ only.
|
157
|
+
# if message.respond_to?(:deliver_now)
|
158
|
+
# message.deliver_now
|
159
|
+
# else
|
160
|
+
# message.deliver
|
161
|
+
# end
|
162
|
+
# end
|
163
|
+
#
|
164
|
+
# # Empty the pending notifications array because the
|
165
|
+
# # after_commit hook can be called multiple times which
|
166
|
+
# # could cause multiple emails to be sent.
|
167
|
+
# pending_notifications.clear
|
168
|
+
# end
|
169
|
+
#
|
170
|
+
# def pending_notifications
|
171
|
+
# \@pending_notifications ||= []
|
172
|
+
# end
|
173
|
+
# end
|
174
|
+
#
|
175
|
+
def send_auth_notification(notification, *args)
|
176
|
+
message = auth_mailer.send(notification, self, *args)
|
177
|
+
message.deliver_now
|
178
|
+
end
|
179
|
+
|
180
|
+
# @!group Callbacks
|
181
|
+
|
182
|
+
# Downcase keys before validation
|
183
|
+
def downcase_keys
|
184
|
+
self.class.case_insensitive_keys.each { |k| _apply(k, :downcase) }
|
185
|
+
end
|
186
|
+
|
187
|
+
# Strip whitespace from requested keys before validation
|
188
|
+
def strip_whitespace
|
189
|
+
self.class.strip_whitespace_keys.each { |k| _apply(k, :strip) }
|
190
|
+
end
|
191
|
+
|
192
|
+
# @!endgroup Callbacks
|
193
|
+
|
194
|
+
private
|
195
|
+
|
196
|
+
# Apply `method` to `attr`
|
197
|
+
def _apply(attr, method)
|
198
|
+
if self[attr]
|
199
|
+
self[attr] = self[attr].try(method)
|
200
|
+
elsif respond_to?(attr) && respond_to?("#{attr}=")
|
201
|
+
new_value = send(attr).try(method)
|
202
|
+
send("#{attr}=", new_value)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
# Class Methods for the `Base` module.
|
207
|
+
module ClassMethods
|
208
|
+
Negroni::Models.config(self,
|
209
|
+
:authentication_keys,
|
210
|
+
:strip_whitespace_keys,
|
211
|
+
:case_insensitive_keys)
|
212
|
+
|
213
|
+
# Find first record based on conditions given (ie by the sign in form).
|
214
|
+
# This method is always called during an authentication process but
|
215
|
+
# it may be wrapped as well. For instance, database authenticable
|
216
|
+
# provides a `find_for_database_authentication` that wraps a call to
|
217
|
+
# this method. This allows you to customize both database authenticable
|
218
|
+
# or the whole authenticate stack by customize `find_for_authentication.`
|
219
|
+
#
|
220
|
+
# Overwrite to add customized conditions, create a join, or maybe use a
|
221
|
+
# namedscope to filter records while authenticating.
|
222
|
+
#
|
223
|
+
# @example
|
224
|
+
# def self.find_for_authentication(tainted_conditions)
|
225
|
+
# find_first_by_auth_conditions(tainted_conditions, active: true)
|
226
|
+
# end
|
227
|
+
#
|
228
|
+
# Finally, notice that Negroni also queries for users in other scenarios
|
229
|
+
# besides authentication, for example when retrieving an user to send
|
230
|
+
# an e-mail for password reset. In such cases, find_for_authentication
|
231
|
+
# is not called.
|
232
|
+
def find_for_authentication(tainted_conditions)
|
233
|
+
find_first_by_auth_conditions(tainted_conditions)
|
234
|
+
end
|
235
|
+
|
236
|
+
# Find the first record, given a set of `tainted_conditions`, and
|
237
|
+
# options.
|
238
|
+
#
|
239
|
+
# @param tainted_conditions [Hash, ActionDispatch::Parameters]
|
240
|
+
# the conditions used to find the record
|
241
|
+
# @param options [Hash] additional conditions and options for the find
|
242
|
+
# operation
|
243
|
+
#
|
244
|
+
# @return [Object] the first record returned from the database
|
245
|
+
def find_first_by_auth_conditions(tainted_conditions, options = {})
|
246
|
+
to_adapter.find_first(
|
247
|
+
_param_filter.filter(tainted_conditions).merge(options)
|
248
|
+
)
|
249
|
+
end
|
250
|
+
|
251
|
+
# Find or initialize a record setting an error if it can't be found.
|
252
|
+
def find_or_initialize_with_error_by(attr, value, error = :invalid)
|
253
|
+
find_or_initialize_with_errors([attr], { attr => value }, error)
|
254
|
+
end
|
255
|
+
|
256
|
+
# Find or initialize a record with a group of attributes, based on a
|
257
|
+
# list of required attributes
|
258
|
+
def find_or_initialize_with_errors(required, attrs, error = :invalid)
|
259
|
+
attrs = _indifferently(required, attrs).delete_if { |_, v| v.blank? }
|
260
|
+
|
261
|
+
if attrs.size == required.size
|
262
|
+
record = find_first_by_auth_conditions(attrs)
|
263
|
+
end
|
264
|
+
|
265
|
+
unless record
|
266
|
+
record = new
|
267
|
+
|
268
|
+
required.each do |key|
|
269
|
+
value = attrs[key]
|
270
|
+
record.send("#{key}=", value)
|
271
|
+
record.errors.add(key, value.present? ? error : :blank)
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
record
|
276
|
+
end
|
277
|
+
|
278
|
+
# Finds an entity of the including class by a token auth request.
|
279
|
+
# This allows users to sign in with either an email address or
|
280
|
+
# thier username.
|
281
|
+
#
|
282
|
+
# @param request [Request] The request which contains the sign in params.
|
283
|
+
#
|
284
|
+
# @return [Object, nil] The found entity, or `nil` if one was not found.
|
285
|
+
def from_token_request(request)
|
286
|
+
# Bail if there is no `auth` param
|
287
|
+
return nil unless (auth_params = request.params['auth'])
|
288
|
+
|
289
|
+
# find_first_by_auth_conditions(auth_params)
|
290
|
+
|
291
|
+
authentication_keys.each do |key|
|
292
|
+
if (found_key = auth_params[key.to_s])
|
293
|
+
return to_adapter.find_first(key => found_key)
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
protected
|
299
|
+
|
300
|
+
def _param_filter
|
301
|
+
@_param_filter ||= Negroni::ParamFilter.new(
|
302
|
+
case_insensitive_keys, strip_whitespace_keys
|
303
|
+
)
|
304
|
+
end
|
305
|
+
|
306
|
+
private
|
307
|
+
|
308
|
+
def _indifferently(required, attributes)
|
309
|
+
if attributes.respond_to?(:permit!)
|
310
|
+
attributes.slice(*required).permit!.to_h.with_indifferent_access
|
311
|
+
else
|
312
|
+
attributes.with_indifferent_access.slice(*required)
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|
317
|
+
end
|
318
|
+
end
|