File: database_authenticatable.rb

package info (click to toggle)
ruby-devise 4.8.1-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 752 kB
  • sloc: ruby: 3,877; sh: 24; makefile: 11
file content (241 lines) | stat: -rw-r--r-- 8,663 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# frozen_string_literal: true

require 'devise/strategies/database_authenticatable'

module Devise
  module Models
    # Authenticatable Module, responsible for hashing the password and
    # validating the authenticity of a user while signing in.
    #
    # This module defines a `password=` method. This method will hash the argument
    # and store it in the `encrypted_password` column, bypassing any pre-existing
    # `password` column if it exists.
    #
    # == Options
    #
    # DatabaseAuthenticatable adds the following options to +devise+:
    #
    #   * +pepper+: a random string used to provide a more secure hash. Use
    #     `rails secret` to generate new keys.
    #
    #   * +stretches+: the cost given to bcrypt.
    #
    #   * +send_email_changed_notification+: notify original email when it changes.
    #
    #   * +send_password_change_notification+: notify email when password changes.
    #
    # == Examples
    #
    #    User.find(1).valid_password?('password123')         # returns true/false
    #
    module DatabaseAuthenticatable
      extend ActiveSupport::Concern

      included do
        after_update :send_email_changed_notification, if: :send_email_changed_notification?
        after_update :send_password_change_notification, if: :send_password_change_notification?

        attr_reader :password, :current_password
        attr_accessor :password_confirmation
      end

      def initialize(*args, &block)
        @skip_email_changed_notification = false
        @skip_password_change_notification = false
        super
      end

      # Skips sending the email changed notification after_update
      def skip_email_changed_notification!
        @skip_email_changed_notification = true
      end

      # Skips sending the password change notification after_update
      def skip_password_change_notification!
        @skip_password_change_notification = true
      end

      def self.required_fields(klass)
        [:encrypted_password] + klass.authentication_keys
      end

      # Generates a hashed password based on the given value.
      # For legacy reasons, we use `encrypted_password` to store
      # the hashed password.
      def password=(new_password)
        @password = new_password
        self.encrypted_password = password_digest(@password) if @password.present?
      end

      # Verifies whether a password (ie from sign in) is the user password.
      def valid_password?(password)
        Devise::Encryptor.compare(self.class, encrypted_password, password)
      end

      # Set password and password confirmation to nil
      def clean_up_passwords
        self.password = self.password_confirmation = nil
      end

      # Update record attributes when :current_password matches, otherwise
      # returns error on :current_password.
      #
      # This method also rejects the password field if it is blank (allowing
      # users to change relevant information like the e-mail without changing
      # their password). In case the password field is rejected, the confirmation
      # is also rejected as long as it is also blank.
      def update_with_password(params, *options)
        if options.present?
          ActiveSupport::Deprecation.warn <<-DEPRECATION.strip_heredoc
            [Devise] The second argument of `DatabaseAuthenticatable#update_with_password`
            (`options`) is deprecated and it will be removed in the next major version.
            It was added to support a feature deprecated in Rails 4, so you can safely remove it
            from your code.
          DEPRECATION
        end

        current_password = params.delete(:current_password)

        if params[:password].blank?
          params.delete(:password)
          params.delete(:password_confirmation) if params[:password_confirmation].blank?
        end

        result = if valid_password?(current_password)
          update(params, *options)
        else
          assign_attributes(params, *options)
          valid?
          errors.add(:current_password, current_password.blank? ? :blank : :invalid)
          false
        end

        clean_up_passwords
        result
      end

      # Updates record attributes without asking for the current password.
      # Never allows a change to the current password. If you are using this
      # method, you should probably override this method to protect other
      # attributes you would not like to be updated without a password.
      #
      # Example:
      #
      #   def update_without_password(params, *options)
      #     params.delete(:email)
      #     super(params)
      #   end
      #
      def update_without_password(params, *options)
        if options.present?
          ActiveSupport::Deprecation.warn <<-DEPRECATION.strip_heredoc
            [Devise] The second argument of `DatabaseAuthenticatable#update_without_password`
            (`options`) is deprecated and it will be removed in the next major version.
            It was added to support a feature deprecated in Rails 4, so you can safely remove it
            from your code.
          DEPRECATION
        end

        params.delete(:password)
        params.delete(:password_confirmation)

        result = update(params, *options)
        clean_up_passwords
        result
      end

      # Destroy record when :current_password matches, otherwise returns
      # error on :current_password. It also automatically rejects
      # :current_password if it is blank.
      def destroy_with_password(current_password)
        result = if valid_password?(current_password)
          destroy
        else
          valid?
          errors.add(:current_password, current_password.blank? ? :blank : :invalid)
          false
        end

        result
      end

      # A callback initiated after successfully authenticating. This can be
      # used to insert your own logic that is only run after the user successfully
      # authenticates.
      #
      # Example:
      #
      #   def after_database_authentication
      #     self.update_attribute(:invite_code, nil)
      #   end
      #
      def after_database_authentication
      end

      # A reliable way to expose the salt regardless of the implementation.
      def authenticatable_salt
        encrypted_password[0,29] if encrypted_password
      end

      if Devise.activerecord51?
        # Send notification to user when email changes.
        def send_email_changed_notification
          send_devise_notification(:email_changed, to: email_before_last_save)
        end
      else
        # Send notification to user when email changes.
        def send_email_changed_notification
          send_devise_notification(:email_changed, to: email_was)
        end
      end

      # Send notification to user when password changes.
      def send_password_change_notification
        send_devise_notification(:password_change)
      end

    protected

      # Hashes the password using bcrypt. Custom hash functions should override
      # this method to apply their own algorithm.
      #
      # See https://github.com/heartcombo/devise-encryptable for examples
      # of other hashing engines.
      def password_digest(password)
        Devise::Encryptor.digest(self.class, password)
      end

      if Devise.activerecord51?
        def send_email_changed_notification?
          self.class.send_email_changed_notification && saved_change_to_email? && !@skip_email_changed_notification
        end
      else
        def send_email_changed_notification?
          self.class.send_email_changed_notification && email_changed? && !@skip_email_changed_notification
        end
      end

      if Devise.activerecord51?
        def send_password_change_notification?
          self.class.send_password_change_notification && saved_change_to_encrypted_password? && !@skip_password_change_notification
        end
      else
        def send_password_change_notification?
          self.class.send_password_change_notification && encrypted_password_changed? && !@skip_password_change_notification
        end
      end

      module ClassMethods
        Devise::Models.config(self, :pepper, :stretches, :send_email_changed_notification, :send_password_change_notification)

        # We assume this method already gets the sanitized values from the
        # DatabaseAuthenticatable strategy. If you are using this method on
        # your own, be sure to sanitize the conditions hash to only include
        # the proper fields.
        def find_for_database_authentication(conditions)
          find_for_authentication(conditions)
        end
      end
    end
  end
end