NoamB/sorcery

View on GitHub
lib/sorcery/model/submodules/reset_password.rb

Summary

Maintainability
A
0 mins
Test Coverage
module Sorcery
  module Model
    module Submodules
      # This submodule adds the ability to reset password via email confirmation.
      # When the user requests an email is sent to him with a url.
      # The url includes a token, which is also saved with the user's record in the db.
      # The token has configurable expiration.
      # When the user clicks the url in the email, providing the token has not yet expired,
      # he will be able to reset his password via a form.
      #
      # When using this submodule, supplying a mailer is mandatory.
      module ResetPassword
        def self.included(base)
          base.sorcery_config.class_eval do
            attr_accessor :reset_password_token_attribute_name,              # reset password code attribute name.
                          :reset_password_token_expires_at_attribute_name,   # expires at attribute name.
                          :reset_password_email_sent_at_attribute_name,      # when was email sent, used for hammering
                                                                             # protection.

                          :reset_password_mailer,                            # mailer class. Needed.

                          :reset_password_mailer_disabled,                   # when true sorcery will not automatically
                                                                             # email password reset details and allow you to
                                                                             # manually handle how and when email is sent

                          :reset_password_email_method_name,                 # reset password email method on your
                                                                             # mailer class.

                          :reset_password_expiration_period,                 # how many seconds before the reset request
                                                                             # expires. nil for never expires.

                          :reset_password_time_between_emails                # hammering protection, how long to wait
                                                                             # before allowing another email to be sent.

          end

          base.sorcery_config.instance_eval do
            @defaults.merge!(:@reset_password_token_attribute_name            => :reset_password_token,
                             :@reset_password_token_expires_at_attribute_name => :reset_password_token_expires_at,
                             :@reset_password_email_sent_at_attribute_name    => :reset_password_email_sent_at,
                             :@reset_password_mailer                          => nil,
                             :@reset_password_mailer_disabled                 => false,
                             :@reset_password_email_method_name               => :reset_password_email,
                             :@reset_password_expiration_period               => nil,
                             :@reset_password_time_between_emails             => 5 * 60 )

            reset!
          end

          base.extend(ClassMethods)

          base.sorcery_config.after_config << :validate_mailer_defined
          base.sorcery_config.after_config << :define_reset_password_fields

          base.send(:include, InstanceMethods)

        end

        module ClassMethods
          # Find user by token, also checks for expiration.
          # Returns the user if token found and is valid.
          def load_from_reset_password_token(token)
            token_attr_name = @sorcery_config.reset_password_token_attribute_name
            token_expiration_date_attr = @sorcery_config.reset_password_token_expires_at_attribute_name
            load_from_token(token, token_attr_name, token_expiration_date_attr)
          end

          protected

          # This submodule requires the developer to define his own mailer class to be used by it
          # when reset_password_mailer_disabled is false
          def validate_mailer_defined
            msg = "To use reset_password submodule, you must define a mailer (config.reset_password_mailer = YourMailerClass)."
            raise ArgumentError, msg if @sorcery_config.reset_password_mailer == nil and @sorcery_config.reset_password_mailer_disabled == false
          end

          def define_reset_password_fields
            sorcery_adapter.define_field sorcery_config.reset_password_token_attribute_name, String
            sorcery_adapter.define_field sorcery_config.reset_password_token_expires_at_attribute_name, Time
            sorcery_adapter.define_field sorcery_config.reset_password_email_sent_at_attribute_name, Time
          end

        end

        module InstanceMethods
          # generates a reset code with expiration
          def generate_reset_password_token!
            config = sorcery_config
            attributes = {config.reset_password_token_attribute_name => TemporaryToken.generate_random_token,
                          config.reset_password_email_sent_at_attribute_name => Time.now.in_time_zone}
            attributes[config.reset_password_token_expires_at_attribute_name] = Time.now.in_time_zone + config.reset_password_expiration_period if config.reset_password_expiration_period

            self.sorcery_adapter.update_attributes(attributes)
          end

          # generates a reset code with expiration and sends an email to the user.
          def deliver_reset_password_instructions!
            mail = false
            config = sorcery_config
            # hammering protection
            return false if config.reset_password_time_between_emails.present? && self.send(config.reset_password_email_sent_at_attribute_name) && self.send(config.reset_password_email_sent_at_attribute_name) > config.reset_password_time_between_emails.seconds.ago.utc
            self.class.sorcery_adapter.transaction do
              generate_reset_password_token!
              mail = send_reset_password_email! unless config.reset_password_mailer_disabled
            end
            mail
          end

          # Clears token and tries to update the new password for the user.
          def change_password!(new_password)
            clear_reset_password_token
            self.send(:"#{sorcery_config.password_attribute_name}=", new_password)
            sorcery_adapter.save
          end

          protected

          def send_reset_password_email!
            generic_send_email(:reset_password_email_method_name, :reset_password_mailer)
          end

          # Clears the token.
          def clear_reset_password_token
            config = sorcery_config
            self.send(:"#{config.reset_password_token_attribute_name}=", nil)
            self.send(:"#{config.reset_password_token_expires_at_attribute_name}=", nil) if config.reset_password_expiration_period
          end
        end

      end
    end
  end
end