negroni 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|