# frozen-string-literal: true
module Rodauth
Base = Feature.define(:base) do
before 'rodauth'
error_flash "Please login to continue", 'require_login'
auth_value_method :account_id_column, :id
auth_value_method :account_open_status_value, 2
auth_value_method :account_password_hash_column, nil
auth_value_method :account_select, nil
auth_value_method :account_status_column, :status_id
auth_value_method :account_unverified_status_value, 1
auth_value_method :accounts_table, :accounts
auth_value_method :default_redirect, '/'
auth_value_method :invalid_password_message, "invalid password"
auth_value_method :login_column, :email
auth_value_method :password_hash_id_column, :id
auth_value_method :password_hash_column, :password_hash
auth_value_method :password_hash_table, :account_password_hashes
auth_value_method :no_matching_login_message, "no matching login"
auth_value_method :login_param, 'login'
auth_value_method :login_label, 'Login'
auth_value_method :password_label, 'Password'
auth_value_method :password_param, 'password'
auth_value_method :modifications_require_password?, true
auth_value_method :session_key, :account_id
auth_value_method :prefix, ''
auth_value_method :require_bcrypt?, true
auth_value_method :skip_status_checks?, true
auth_value_method :title_instance_variable, nil
auth_value_method :unverified_account_message, "unverified account, please verify account before logging in"
redirect(:require_login){"#{prefix}/login"}
auth_value_methods(
:db,
:require_login_redirect,
:set_deadline_values?,
:use_date_arithmetic?,
:use_database_authentication_functions?
)
auth_methods(
:account_id,
:account_session_value,
:already_logged_in,
:authenticated?,
:clear_session,
:csrf_tag,
:function_name,
:logged_in?,
:login_required,
:open_account?,
:password_match?,
:random_key,
:redirect,
:session_value,
:set_error_flash,
:set_notice_flash,
:set_notice_now_flash,
:set_redirect_error_flash,
:set_title,
:unverified_account_message,
:update_session
)
auth_private_methods(
:account_from_login,
:account_from_session
)
configuration_module_eval do
def auth_class_eval(&block)
auth.class_eval(&block)
end
def account_model(model)
warn "account_model is deprecated, use db and accounts_table settings"
db model.db
accounts_table model.table_name
account_select model.dataset.opts[:select]
end
end
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
def route!
if meth = self.class.route_hash[request.remaining_path]
send(meth)
end
nil
end
def set_field_error(field, error)
(@field_errors ||= {})[field] = error
end
def field_error(field)
return nil unless @field_errors
@field_errors[field]
end
def account_id
account[account_id_column]
end
alias account_session_value account_id
def session_value
session[session_key]
end
alias logged_in? session_value
def account_from_login(login)
@account = _account_from_login(login)
end
def open_account?
skip_status_checks? || account[account_status_column] == account_open_status_value
end
def db
Sequel::DATABASES.first
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 check_already_logged_in
already_logged_in if logged_in?
end
def already_logged_in
nil
end
def clear_session
session.clear
end
def login_required
set_redirect_error_flash require_login_error_flash
redirect require_login_redirect
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 set_notice_now_flash(message)
flash.now[:notice] = message
end
def require_login
login_required unless logged_in?
end
def authenticated?
logged_in?
end
def require_authentication
require_login
end
def account_initial_status_value
account_open_status_value
end
def account_from_session
@account = _account_from_session
end
def csrf_tag
scope.csrf_tag if scope.respond_to?(:csrf_tag)
end
def button(value, opts={})
opts = {:locals=>{:value=>value, :opts=>opts}}
opts[:path] = template_path('button')
scope.render(opts)
end
def view(page, title)
set_title(title)
_view(:view, page)
end
def render(page)
_view(:render, page)
end
def post_configure
require 'bcrypt' if require_bcrypt?
db.extension :date_arithmetic if use_date_arithmetic?
route_hash= {}
self.class.routes.each do |meth|
route_hash["/#{send("#{meth.to_s.sub(/\Ahandle_/, '')}_route")}"] = meth
end
self.class.route_hash = route_hash.freeze
end
def password_match?(password)
if account_password_hash_column
BCrypt::Password.new(account[account_password_hash_column]) == password
elsif use_database_authentication_functions?
id = account_id
if salt = db.get(Sequel.function(function_name(:rodauth_get_salt), id))
hash = BCrypt::Engine.hash_secret(password, salt)
db.get(Sequel.function(function_name(:rodauth_valid_password_hash), id, hash))
end
else
# :nocov:
if hash = password_hash_ds.get(password_hash_column)
BCrypt::Password.new(hash) == password
end
# :nocov:
end
end
private
def update_session
clear_session
session[session_key] = account_session_value
end
# Return a string for the parameter name. This will be an empty
# string if the parameter doesn't exist.
def param(key)
param_or_nil(key).to_s
end
# Return a string for the parameter name, or nil if there is no
# parameter with that name.
def param_or_nil(key)
value = request.params[key]
value.to_s unless value.nil?
end
def redirect(path)
request.redirect(path)
end
def transaction(opts={}, &block)
db.transaction(opts, &block)
end
if RUBY_VERSION >= '1.9'
def random_key
SecureRandom.urlsafe_base64(32)
end
else
# :nocov:
def random_key
SecureRandom.hex(32)
end
# :nocov:
end
def timing_safe_eql?(provided, actual)
provided = provided.to_s
Rack::Utils.secure_compare(provided.ljust(actual.length), actual) && provided.length == actual.length
end
def require_account
require_authentication
require_account_session
end
def require_account_session
unless account_from_session
clear_session
login_required
end
end
def catch_error(&block)
catch(:rodauth_error, &block)
end
def throw_error(field, error)
set_field_error(field, error)
throw :rodauth_error
end
def use_date_arithmetic?
set_deadline_values?
end
def set_deadline_values?
db.database_type == :mysql
end
def use_database_authentication_functions?
case db.database_type
when :postgres, :mysql, :mssql
true
else
# :nocov:
false
# :nocov:
end
end
def function_name(name)
if db.database_type == :mssql
# :nocov:
"dbo.#{name}"
# :nocov:
else
name
end
end
def _account_from_login(login)
ds = db[accounts_table].where(login_column=>login)
ds = ds.select(*account_select) if account_select
ds = ds.where(account_status_column=>[account_unverified_status_value, account_open_status_value]) unless skip_status_checks?
ds.first
end
def _account_from_session
ds = account_ds(session_value)
ds = ds.where(account_session_status_filter) unless skip_status_checks?
ds.first
end
def account_session_status_filter
{account_status_column=>account_open_status_value}
end
def template_path(page)
File.join(File.dirname(__FILE__), '../../../templates', "#{page}.str")
end
def account_ds(id=account_id)
raise ArgumentError, "invalid account id passed to account_ds" unless id
ds = db[accounts_table].where(account_id_column=>id)
ds = ds.select(*account_select) if account_select
ds
end
def password_hash_ds
db[password_hash_table].where(password_hash_id_column=>account_id)
end
# This is needed for jdbc/sqlite, which returns timestamp columns as strings
def convert_timestamp(timestamp)
timestamp = db.to_application_timestamp(timestamp) if timestamp.is_a?(String)
timestamp
end
# This is used to avoid race conditions when using the pattern of inserting when
# an update affects no rows. In such cases, if a row is inserted between the
# update and the insert, the insert will fail with a uniqueness error, but
# retrying will work. It is possible for it to fail again, but only if the row
# is deleted before the update and readded before the insert, which is very
# unlikely to happen. In such cases, raising an exception is acceptable.
def retry_on_uniqueness_violation(&block)
if raises_uniqueness_violation?(&block)
yield
end
end
# In cases where retrying on uniqueness violations cannot work, this will detect
# whether a uniqueness violation is raised by the block and return the exception if so.
# This method should be used if you don't care about the exception itself.
def raises_uniqueness_violation?(&block)
transaction(:savepoint=>:only, &block)
false
rescue unique_constraint_violation_class => e
e
end
# Work around jdbc/sqlite issue where it only raises ConstraintViolation and not
# UniqueConstraintViolation.
def unique_constraint_violation_class
if db.adapter_scheme == :jdbc && db.database_type == :sqlite
# :nocov:
Sequel::ConstraintViolation
# :nocov:
else
Sequel::UniqueConstraintViolation
end
end
# If you would like to operate/reraise the exception, this alias makes more sense.
alias raised_uniqueness_violation raises_uniqueness_violation?
# If you just want to ignore uniqueness violations, this alias makes more sense.
alias ignore_uniqueness_violation raises_uniqueness_violation?
# This is needed on MySQL, which doesn't support non constant defaults other than
# CURRENT_TIMESTAMP.
def set_deadline_value(hash, column, interval)
if set_deadline_values?
# :nocov:
hash[column] = Sequel.date_add(Sequel::CURRENT_TIMESTAMP, interval)
# :nocov:
end
end
def set_session_value(key, value)
session[key] = value
end
def update_hash_ds(hash, ds, values)
num = ds.update(values)
if num == 1
values.each do |k, v|
account[k] = v == Sequel::CURRENT_TIMESTAMP ? Time.now : v
end
end
num
end
def update_account(values, ds=account_ds)
update_hash_ds(account, ds, values)
end
def _view(meth, page)
auth = self
auth_template_path = template_path(page)
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] = auth_template_path
end
send(meth, template_opts)
end
end
end
end