lib/devise/models/magic_link_authenticatable.rb
require "devise/strategies/magic_link_authenticatable"
require "devise/hooks/magic_link_authenticatable"

module Devise
  module Models
    module MagicLinkAuthenticatable
      extend ActiveSupport::Concern

      # Models using the :database_authenticatable strategy will already
      # have #password_required? and #password defined - we will defer
      # to those methods if they already exist so that people can use
      # both strategies together. Otherwise, for :magic_link_authenticatable-
      # only users, we define them in order to disable password validations:

      unless instance_methods.include?(:password_required?)
        def password_required?
          false
        end
      end

      unless instance_methods.include?(:password)
        # Not having a #password method breaks the :validatable module
        #
        # NOTE I proposed a change to Devise to fix this:
        # https://github.com/heartcombo/devise/issues/5346#issuecomment-822022834
        # As of yet it hasn't been accepted due to unknowns of the legacy code's purpose
        def password
          nil
        end
      end

      def encode_passwordless_token(*args, **kwargs)
        self.class.passwordless_tokenizer_class.encode(self, *args, **kwargs)
      end

      def send_magic_link(remember_me: false, **kwargs)
        token = encode_passwordless_token
        send_devise_notification(:magic_link, token, remember_me, **kwargs)
      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_magic_link_authentication
      #     self.update_attribute(:invite_code, nil)
      #   end
      #
      def after_magic_link_authentication
      end

      # Set this to false to disable magic link auth for this model instance.
      # Magic links will still be generated by the sign-in page, but visiting
      # them will instead display an error message.
      def active_for_magic_link_authentication?
        true
      end

      # This method determines which error message to display when magic link
      # auth is disabled for this model instance.
      def magic_link_inactive_message
        :magic_link_invalid
      end

      protected

      module ClassMethods
        def passwordless_tokenizer_class
          @passwordless_tokenizer_class ||= if passwordless_tokenizer.is_a?(Class)

            passwordless_tokenizer

          else
            (
                        passwordless_tokenizer.start_with?("::") ? (
                          passwordless_tokenizer.constantize
                        ) : (
                          "Devise::Passwordless::#{passwordless_tokenizer}".constantize
                        )
                      )
          end
        end

        def decode_passwordless_token(*args, **kwargs)
          passwordless_tokenizer_class.decode(*args, **kwargs)
        end

        # We assume this method already gets the sanitized values from the
        # MagicLinkAuthenticatable 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_magic_link_authentication(conditions)
          find_for_authentication(conditions)
        end

        Devise::Models.config(self,
          :passwordless_tokenizer,
          :passwordless_login_within,
          # :passwordless_secret_key,
          :passwordless_expire_old_tokens_on_sign_in)
      end
    end
  end
end

module Devise
  mattr_accessor :passwordless_tokenizer
  @@passwordless_tokenizer = nil
  def self.passwordless_tokenizer
    if @@passwordless_tokenizer.blank?
      Devise::Passwordless.deprecator.warn <<-DEPRECATION.strip_heredoc
        [Devise-Passwordless] `Devise.passwordless_tokenizer` is a required
        config option. If you are upgrading to Devise-Passwordless 1.0 from
        a previous install, you should use "MessageEncryptorTokenizer" for
        backwards compatibility. New installs are templated with
        "SignedGlobalIDTokenizer". Read the README for a comparison of
        options and UPGRADING for upgrade instructions. Execution will
        now proceed with a value of "MessageEncryptorTokenizer" but future
        releases will raise an error if this option is unset.
      DEPRECATION

      "MessageEncryptorTokenizer"
    else
      @@passwordless_tokenizer
    end
  end

  mattr_accessor :passwordless_login_within
  @@passwordless_login_within = 20.minutes

  mattr_accessor :passwordless_secret_key
  @@passwordless_secret_key = nil

  mattr_accessor :passwordless_expire_old_tokens_on_sign_in
  @@passwordless_expire_old_tokens_on_sign_in = false
end
