lib/roda/plugins/rodauth/base.rb



class Roda
  module RodaPlugins
    module Rodauth
      Base = Feature.define(:base) do
        auth_value_methods(
          :account_id,
          :account_model,
          :account_open_status_value,
          :account_password_hash_column,
          :account_status_id,
          :account_unverified_status_value,
          :default_redirect,
          :email_from,
          :email_subject_prefix,
          :login_column,
          :login_confirm_label,
          :login_confirm_param,
          :login_label,
          :login_param,
          :logins_do_not_match_message,
          :no_matching_login_message,
          :password_confirm_label,
          :password_confirm_param,
          :password_does_not_meet_requirements_message,
          :password_hash_column,
          :password_hash_cost,
          :password_hash_table,
          :password_label,
          :password_minimum_length,
          :password_param,
          :passwords_do_not_match_message,
          :prefix,
          :require_login_notice_message,
          :require_login_redirect,
          :session_key,
          :skip_status_checks?,
          :title_instance_variable
        )

        auth_methods(
          :account_from_login,
          :account_from_session,
          :account_id_value,
          :account_session_value,
          :after_close_account,
          :already_logged_in,
          :clear_session,
          :create_email,
          :email_to,
          :logged_in?,
          :login_errors_message,
          :login_required,
          :open_account?,
          :password_hash,
          :password_meets_requirements?,
          :random_key,
          :session_value,
          :set_error_flash,
          :set_notice_flash,
          :set_password,
          :set_redirect_error_flash,
          :set_title,
          :unverified_account_message,
          :update_session
        )

        attr_reader :scope
        attr_reader :account

        def initialize(scope)
          @scope = scope
        end

        def features
          self.class.features
        end

        def request
          scope.request
        end

        def response
          scope.response
        end

        def session
          scope.session
        end

        def flash
          scope.flash
        end

        # Overridable methods

        def account_id_value
          account.send(account_id)
        end
        alias account_session_value account_id_value

        def session_value
          session[session_key]
        end

        def account_status_id_value
          account.send(account_status_id)
        end

        def _account_from_login(login)
          @account = account_from_login(login)
        end

        def account_from_login(login)
          ds = account_model.where(login_column=>login)
          ds = ds.where(account_status_id=>[account_unverified_status_value, account_open_status_value]) unless skip_status_checks?
          ds.first
        end

        def open_account?
          skip_status_checks? || account_status_id_value == account_open_status_value 
        end

        def unverified_account_message
          "unverified account, please verify account before logging in"
        end

        def update_session
          clear_session
          session[session_key] = account_session_value
        end

        def check_before(feature)
          meth = :"check_before_#{feature.feature_name}"
          if respond_to?(meth)
            send(meth)
          elsif feature.account_required?
            require_account
          elsif logged_in?
            already_logged_in 
          end
        end

        def account_model
          ::Account
        end

        def db
          account_model.db
        end

        # If the account_password_hash_column is set, the password hash is verified in
        # ruby, it will not use a database function to do so, it will check the password
        # hash using bcrypt.
        def account_password_hash_column
          nil
        end

        def already_logged_in
          nil
        end

        def clear_session
          session.clear
        end

        def default_redirect
          '/'
        end

        def require_login_redirect
          "#{prefix}/login"
        end

        def require_login_notice_message
          "Please login to continue"
        end

        def prefix
          ''
        end

        def login_required
          set_notice_flash require_login_notice_message
          request.redirect require_login_redirect
        end

        def random_key
          require 'securerandom'
          if RUBY_VERSION >= '1.9'
            SecureRandom.urlsafe_base64(32)
          else
            # :nocov:
            SecureRandom.hex(32)
            # :nocov:
          end
        end

        def title_instance_variable
          nil
        end

        def set_title(title)
          if title_instance_variable
            scope.instance_variable_set(title_instance_variable, title)
          end
        end

        def set_error_flash(message)
          flash.now[:error] = message
        end

        def set_redirect_error_flash(message)
          flash[:error] = message
        end

        def set_notice_flash(message)
          flash[:notice] = message
        end

        def login_column
          :email
        end

        def password_hash_column
          :password_hash
        end

        def password_hash_table
          :account_password_hashes
        end

        def no_matching_login_message
          "no matching login"
        end

        def logged_in?
          session[session_key]
        end

        def require_login
          login_required unless logged_in?
        end

        def require_account
          require_login
          unless _account_from_session
            clear_session
            login_required
          end
        end

        def login_param
          'login'
        end

        def login_confirm_param
          'login-confirm'
        end

        def login_label
          'Login'
        end

        def login_confirm_label
          "Confirm #{login_label}"
        end

        def password_label
          'Password'
        end

        def password_confirm_label
          "Confirm #{password_label}"
        end

        def login_errors_message
          if errors = account.errors.on(login_column)
            errors.join(', ')
          end
        end

        def logins_do_not_match_message
          'logins do not match'
        end

        def password_param
          'password'
        end

        def password_confirm_param
          'password-confirm'
        end

        def session_key
          :account_id
        end

        def account_id
          :id
        end

        def account_status_id
          :status_id
        end

        def passwords_do_not_match_message
          'passwords do not match'
        end

        def password_does_not_meet_requirements_message
          "invalid password, does not meet requirements (minimum #{password_minimum_length} characters)"
        end

        def password_minimum_length
          6
        end

        def password_meets_requirements?(password)
          password_minimum_length <= password.length
        end

        def account_unverified_status_value
          1
        end

        def account_open_status_value
          2
        end

        def account_initial_status_value
          account_open_status_value
        end

        def _account_from_session
          @account = account_from_session
        end

        def account_from_session
          ds = account_model.where(account_id=>scope.session[session_key])
          ds = ds.where(account_status_id=>account_open_status_value) unless skip_status_checks?
          ds.first
        end

        def password_hash_cost
          require 'bcrypt'
          if ENV['RACK_ENV'] == 'test'
            BCrypt::Engine::MIN_COST
          else
            # :nocov:
            BCrypt::Engine::DEFAULT_COST
            # :nocov:
          end
        end

        def password_hash(password)
          require 'bcrypt'
          BCrypt::Password.create(password, :cost=>password_hash_cost)
        end

        def set_password(password)
          hash = password_hash(password)
          if account_password_hash_column
            account.set(account_password_hash_column=>hash).save_changes(:raise_on_save_failure=>true)
          else
            if db[password_hash_table].where(account_id=>account_id_value).update(password_hash_column=>hash) == 0
              db[password_hash_table].insert(account_id=>account_id_value, password_hash_column=>hash)
            end
          end
        end

        def transaction(&block)
          db.transaction(&block)
        end

        def email_from
          "webmaster@#{request.host}"
        end

        def email_to
          account.email
        end

        def create_email(subject, body)
          require 'mail'
          m = Mail.new
          m.from = email_from
          m.to = email_to
          m.subject = "#{email_subject_prefix}#{subject}"
          m.body = body
          m
        end

        def email_subject_prefix
          nil
        end

        def view(page, title)
          set_title(title)
          _view(:view, page)
        end

        def render(page)
          _view(:render, page)
        end

        def skip_status_checks?
          false
        end

        def after_close_account
        end

        private

        def _view(meth, page)
          auth = self
          scope.instance_exec do
            template_opts = find_template(parse_template_opts(page, :locals=>{:rodauth=>auth}))
            unless File.file?(template_path(template_opts))
              template_opts[:path] = File.join(File.dirname(__FILE__), '../../../../templates', "#{page}.str")
            end
            send(meth, template_opts)
          end
        end
      end
    end
  end
end