Skip to content

Instantly share code, notes, and snippets.

@bjeanes
Last active July 14, 2021 00:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bjeanes/85eabe7fb03e73cb9b1e6bc7cc81a0f8 to your computer and use it in GitHub Desktop.
Save bjeanes/85eabe7fb03e73cb9b1e6bc7cc81a0f8 to your computer and use it in GitHub Desktop.
Authentication.api(some_user).change_login(new_login: 'user@example.com')

Authentication.api(:admin).create_account(login: 'admin@example.com')

Authentication.api(:user).login_path # => '/log_in'

# verify_account without knowing emailing the user or knowing the key that would be generated
Authentication.api(user) do
  transaction do
    before_verify_account
    verify_account
    remove_verify_account_key
    after_verify_account
  end
end
module Authentication
def self.api(*args, **opts, &blk)
api = API.new(*args, **opts)
return api.internal(&blk) if block_given?
api
end
class API
def initialize(
config_or_record = :user,
actor: nil,
reason: nil,
env: {},
session: {}
)
config_name, account = resolve_config_name_and_account(config_or_record)
# RequestContext.request will be the parent request in Rails, which assists in my custom auditing information capture
env = (RequestContext.request&.env || {}).merge(env)
actor ||= RequestContext.actor
env.merge!("internal.audit_actor" => actor) if actor
env.merge!("internal.audit_reason" => reason) if reason
@account = account
@session = session
@env = env
@config_name = config_name.to_sym
@auth_class = RodauthConfig.opts[:rodauths].fetch(@config_name)
end
def internal(params: {}, &blk)
# FIXME: can I build this based on @auth_class without duplicating the session special-casing
# which internal_request does?
instance = Rodauth::Rails.rodauth(
@config_name,
account: @account,
env: @env.except("rack.input", "QUERY_STRING"), # rodauth-rails doesn't replace these, so need to exclude
form: params,
session: @session
)
instance.instance_eval(&blk) if block_given?
instance
end
private
def resolve_config_name_and_account(config_or_record)
case config_or_record
when :admin, :user
[config_or_record, nil]
when User, Admin # NOTE: object can be wrapped with Draper; this matches either way
config = config_or_record.model_name.param_key.to_sym # this approach for both decorated and naked objects
account = Account.find(config_or_record.id)
[config, account]
when Account
account = config_or_record
[(account.user ? :user : :admin), account]
else
raise ArgumentError, "expected config name or instance of User or Admin"
end
end
def method_missing(meth, *args, **opts, &blk)
if delegated_class_method?(meth)
opts[:account_id] = @account.id if @account
opts[:env] = @env.merge(opts[:env] || {})
opts[:session] = @session.merge(opts[:session] || {})
@auth_class.send(meth, *args, **opts, &blk)
elsif delegated_instance_method?(meth)
internal.send(meth, *args, **opts, &blk)
else
super
end
end
def respond_to_missing?(meth, _include_private = false)
delegated_class_method?(meth) || delegated_instance_method?(meth) || super
end
def delegated_class_method?(meth)
# This _should_ only match internal request methods, but this has only been anecdotally demonstrated
@auth_class.methods(_ancestors = false).include?(meth)
end
def delegated_instance_method?(meth)
return false unless meth =~ /_path$|_url$/ # We only delegate _path and _url methods
@auth_class.instance_methods(_ancestors = true).include?(meth)
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment