authenticatable 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.md +19 -0
- data/README.md +144 -0
- data/app/controllers/authenticatable/passwords_controller.rb +51 -0
- data/app/controllers/authenticatable/registrations_controller.rb +28 -0
- data/app/controllers/authenticatable/sessions_controller.rb +49 -0
- data/app/controllers/authenticatable_controller.rb +101 -0
- data/app/mailers/authenticatable/mailer.rb +17 -0
- data/app/views/authenticatable/mailer/reset_password.html.erb +8 -0
- data/app/views/authenticatable/passwords/_edit_form.html.erb +6 -0
- data/app/views/authenticatable/passwords/_new_form.html.erb +6 -0
- data/app/views/authenticatable/passwords/edit.html.erb +7 -0
- data/app/views/authenticatable/passwords/new.html.erb +7 -0
- data/app/views/authenticatable/registrations/_form.html.erb +12 -0
- data/app/views/authenticatable/registrations/new.html.erb +5 -0
- data/app/views/authenticatable/sessions/_form.html.erb +5 -0
- data/app/views/authenticatable/sessions/new.html.erb +7 -0
- data/app/views/authenticatable/shared/_errors.html.erb +7 -0
- data/app/views/authenticatable/shared/_flash_messages.html.erb +3 -0
- data/app/views/authenticatable/shared/_links.html.erb +12 -0
- data/config/locales/en.yml +14 -0
- data/lib/authenticatable/controllers/helpers.rb +72 -0
- data/lib/authenticatable/controllers/url_helpers.rb +67 -0
- data/lib/authenticatable/controllers.rb +9 -0
- data/lib/authenticatable/engine.rb +17 -0
- data/lib/authenticatable/errors/unauthenticated_error.rb +6 -0
- data/lib/authenticatable/errors.rb +6 -0
- data/lib/authenticatable/manager.rb +16 -0
- data/lib/authenticatable/models/email_validator.rb +17 -0
- data/lib/authenticatable/models/identifier.rb +43 -0
- data/lib/authenticatable/models/password.rb +73 -0
- data/lib/authenticatable/models.rb +67 -0
- data/lib/authenticatable/proxy.rb +103 -0
- data/lib/authenticatable/rails/routes.rb +61 -0
- data/lib/authenticatable/rspec.rb +8 -0
- data/lib/authenticatable/scope.rb +110 -0
- data/lib/authenticatable/serializers/base.rb +39 -0
- data/lib/authenticatable/serializers/session.rb +36 -0
- data/lib/authenticatable/serializers.rb +9 -0
- data/lib/authenticatable/testing/controller_helpers.rb +31 -0
- data/lib/authenticatable/token.rb +13 -0
- data/lib/authenticatable/version.rb +5 -0
- data/lib/authenticatable.rb +100 -0
- data/lib/generators/active_record/authenticatable_generator.rb +63 -0
- data/lib/generators/active_record/templates/migration.tt +15 -0
- data/lib/generators/active_record/templates/migration_existing.tt +23 -0
- data/lib/generators/authenticatable/authenticatable_generator.rb +18 -0
- data/lib/generators/authenticatable/orm_helpers.rb +30 -0
- data/lib/generators/authenticatable/views_generator.rb +19 -0
- metadata +136 -0
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/concern"
|
4
|
+
|
5
|
+
module Authenticatable
|
6
|
+
module Controllers
|
7
|
+
module UrlHelpers
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
included do
|
11
|
+
if respond_to?(:helper_method)
|
12
|
+
helper_method :session_path, :new_session_path,
|
13
|
+
:registration_path, :new_registration_path,
|
14
|
+
:new_password_path, :password_path, :edit_password_url, :update_password_path
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Helper methods to generate a generic path helpers dynamically
|
19
|
+
# in views, based on the current scope.
|
20
|
+
# Example: session_path('user') => user_session_path
|
21
|
+
def new_session_path(resource_name)
|
22
|
+
:"new_#{resource_name}_session"
|
23
|
+
end
|
24
|
+
|
25
|
+
def session_path(resource_name)
|
26
|
+
:"#{resource_name}_session"
|
27
|
+
end
|
28
|
+
|
29
|
+
def new_registration_path(resource_name)
|
30
|
+
:"new_#{resource_name}_registration"
|
31
|
+
end
|
32
|
+
|
33
|
+
def registration_path(resource_name)
|
34
|
+
:"#{resource_name}_registration"
|
35
|
+
end
|
36
|
+
|
37
|
+
def new_password_path(resource_name)
|
38
|
+
:"new_#{resource_name}_password"
|
39
|
+
end
|
40
|
+
|
41
|
+
def password_path(resource_name)
|
42
|
+
:"#{resource_name}_password"
|
43
|
+
end
|
44
|
+
|
45
|
+
def update_password_path(resource_name, token)
|
46
|
+
public_send("update_#{resource_name}_password_path", token: token)
|
47
|
+
end
|
48
|
+
|
49
|
+
def edit_password_url(resource_name, token)
|
50
|
+
public_send("edit_#{resource_name}_password_url", token: token)
|
51
|
+
end
|
52
|
+
|
53
|
+
# End generic path helpers.
|
54
|
+
|
55
|
+
# The default url to redirect to after sign in. This URL
|
56
|
+
# can be overriden in your ApplicationController like this:
|
57
|
+
#
|
58
|
+
# def after_sign_in_path(resource)
|
59
|
+
# dashboard_path
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
def after_sign_in_path(_resource, _resource_name)
|
63
|
+
root_url
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Authenticatable
|
4
|
+
# Makes Authenticatable available to Rails on initializing by injecting the
|
5
|
+
# Authenticatable manager into Rack middlewares.
|
6
|
+
class Engine < ::Rails::Engine
|
7
|
+
config.app_middleware.use(Authenticatable::Manager)
|
8
|
+
|
9
|
+
initializer "authenticatable.setup" do
|
10
|
+
# Make authenticatable helpers available on controllers.
|
11
|
+
ActionController::Base.include Authenticatable::Controllers::Helpers
|
12
|
+
|
13
|
+
# Make authenticatable method available on models.
|
14
|
+
ActiveRecord::Base.include Authenticatable::Models
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Authenticatable
|
4
|
+
# The middleware for Rack Authentication that injects the authentication
|
5
|
+
# session into the rack environment hash.
|
6
|
+
class Manager
|
7
|
+
def initialize(app)
|
8
|
+
@app = app
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(env)
|
12
|
+
env["authenticatable"] = Authenticatable::Proxy.new(env)
|
13
|
+
@app.call(env)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/concern"
|
4
|
+
require "valid_email2"
|
5
|
+
|
6
|
+
module Authenticatable
|
7
|
+
module Models
|
8
|
+
module EmailValidator
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
|
11
|
+
included do
|
12
|
+
validates :email, presence: true, uniqueness: true
|
13
|
+
validates_format_of :email, with: Authenticatable.config.email_regexp
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/concern"
|
4
|
+
|
5
|
+
module Authenticatable
|
6
|
+
module Models
|
7
|
+
module Identifier
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
# Since the identifier_column can be set dynamically (to for example email or username)
|
11
|
+
# we'll need a common attribute to access the authenticated resource identifier.
|
12
|
+
def identifier
|
13
|
+
self[self.class.identifier_column]
|
14
|
+
end
|
15
|
+
|
16
|
+
class_methods do
|
17
|
+
# Search for a resource by the choosen identifier.
|
18
|
+
# find_by_identifier('foo@bar.com') is equivalent to find_by(email: 'foo@bar.com')
|
19
|
+
def find_by_identifier(value)
|
20
|
+
find_by("#{identifier_column}": normalize_identifier(value))
|
21
|
+
end
|
22
|
+
|
23
|
+
# Ensure identifier is a downcase string without spaces.
|
24
|
+
# Example:
|
25
|
+
# fOo@b aR.com => foo@bar.com
|
26
|
+
def normalize_identifier(identifier)
|
27
|
+
identifier.to_s.downcase.gsub(/\s+/, "")
|
28
|
+
end
|
29
|
+
|
30
|
+
# Identifying column to use when looking up an authenticatable record in the database.
|
31
|
+
# Can be for example email or a username. Default is email.
|
32
|
+
def identifier_column
|
33
|
+
@identifier_column || Authenticatable.config.default_identifier
|
34
|
+
end
|
35
|
+
|
36
|
+
# Convient method to update the identifier_column.
|
37
|
+
def identify_by(column)
|
38
|
+
@identifier_column = column
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/concern"
|
4
|
+
require "bcrypt"
|
5
|
+
|
6
|
+
module Authenticatable
|
7
|
+
module Models
|
8
|
+
module Password
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
|
11
|
+
included do
|
12
|
+
has_secure_password
|
13
|
+
validates_presence_of :password_confirmation, if: :password_digest_changed?
|
14
|
+
validates :password, length: Authenticatable.config.password_length, if: :password_digest_changed?
|
15
|
+
end
|
16
|
+
|
17
|
+
class_methods do
|
18
|
+
def authenticate_with_identifier(identifier, password)
|
19
|
+
if (resource = find_by_identifier(identifier))
|
20
|
+
resource&.authenticate(password) ? resource : nil
|
21
|
+
else
|
22
|
+
prevent_timing_attack
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def find_by_reset_password_token(token)
|
27
|
+
return nil if token.blank?
|
28
|
+
|
29
|
+
expiration_time = Authenticatable.config.reset_password_expiration_time
|
30
|
+
where(reset_password_token: token).where("reset_password_sent_at > ?", expiration_time.ago).first
|
31
|
+
end
|
32
|
+
|
33
|
+
# Hash a random password even when a resource doesn't exist for the given identifier.
|
34
|
+
# This is necessary to protect against timing/enumeration attacks - e.g. the request
|
35
|
+
# is faster when a resource doesn't exist in the database if the password hashing
|
36
|
+
# algorithm is not called.
|
37
|
+
def prevent_timing_attack
|
38
|
+
new(password: "*")
|
39
|
+
nil
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Sets the :reset_password_token attribute to a random token
|
44
|
+
# generated by Authenticatable::Token
|
45
|
+
def flush_reset_password_token
|
46
|
+
self.reset_password_token = Authenticatable::Token.new
|
47
|
+
self.reset_password_sent_at = Time.current
|
48
|
+
end
|
49
|
+
|
50
|
+
# Sets the :reset_password_token attribute to a random token
|
51
|
+
# generated by Authenticatable::Token and saves the record
|
52
|
+
# without running validations.
|
53
|
+
def reset_password_token!
|
54
|
+
flush_reset_password_token
|
55
|
+
save(validate: false)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Validates and sets the new password from secure_params and
|
59
|
+
# sets reset_password_token to nil.
|
60
|
+
def update_password(secure_params)
|
61
|
+
if secure_params[:password].blank?
|
62
|
+
errors.add(:password, :blank)
|
63
|
+
return false
|
64
|
+
end
|
65
|
+
|
66
|
+
self.password = secure_params[:password]
|
67
|
+
self.password_confirmation = secure_params[:password_confirmation]
|
68
|
+
flush_reset_password_token if valid?
|
69
|
+
save
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "authenticatable/models/email_validator"
|
4
|
+
require "authenticatable/models/identifier"
|
5
|
+
require "authenticatable/models/password"
|
6
|
+
require "active_support/concern"
|
7
|
+
|
8
|
+
module Authenticatable
|
9
|
+
module Models
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
attr_accessor :authenticatable_extensions, :authenticatable_loaded_extensions
|
14
|
+
|
15
|
+
def authenticatable(*extensions)
|
16
|
+
opts = extensions.extract_options!
|
17
|
+
|
18
|
+
# Add default extensions to class variable @authenticatable_extensions. These are added to a global accessible
|
19
|
+
# class var, so that we can access them from other places, like in extensions, views or controllers.
|
20
|
+
@authenticatable_extensions = (Authenticatable.config.default_extensions + extensions).uniq
|
21
|
+
|
22
|
+
# We'll add extensions here while they loaded. Can be useful for debugging
|
23
|
+
@authenticatable_loaded_extensions = []
|
24
|
+
|
25
|
+
# Remove extensions that are specified in the :skip attribute from the @authenticatable_extensions
|
26
|
+
# array. This can be useful if you for example want to disable any of the default extensions.
|
27
|
+
opts[:skip] = [opts[:skip]].flatten
|
28
|
+
opts[:skip].each { |s| @authenticatable_extensions.delete(s) } if opts[:skip].present?
|
29
|
+
|
30
|
+
# Load extensions into model
|
31
|
+
load_core_mixins
|
32
|
+
|
33
|
+
# Load extensions into model
|
34
|
+
load_model_extensions
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
# Loads extension concerns/mixins into the model class if it can find a module with
|
40
|
+
# the classified extension name in the Authenticatable::Models namespace.
|
41
|
+
#
|
42
|
+
# Example:
|
43
|
+
# authenticatable extensions: { :email_validator }
|
44
|
+
# will include a module with name (if it exists):
|
45
|
+
# Authenticatable::Models::EmailValidator
|
46
|
+
#
|
47
|
+
def load_model_extensions
|
48
|
+
@authenticatable_extensions.each do |extension|
|
49
|
+
module_name = "Authenticatable::Models::#{extension.to_s.classify}"
|
50
|
+
next unless const_defined?(module_name)
|
51
|
+
|
52
|
+
extension_module = const_get(module_name)
|
53
|
+
include extension_module
|
54
|
+
@authenticatable_loaded_extensions << module_name
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Concerns that should be included to the authenticatable model if
|
59
|
+
# initialized with the 'authenticatable'-method above.
|
60
|
+
def load_core_mixins
|
61
|
+
include Authenticatable::Models::EmailValidator
|
62
|
+
include Authenticatable::Models::Identifier
|
63
|
+
include Authenticatable::Models::Password
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Authenticatable
|
4
|
+
# The authenticatable session, persisted in env["authenticatable"]
|
5
|
+
class Proxy
|
6
|
+
def initialize(env)
|
7
|
+
@env = env
|
8
|
+
@current = {}
|
9
|
+
@config = Authenticatable.config
|
10
|
+
end
|
11
|
+
|
12
|
+
# Check if the given scope is already signed in or else run authenticated strategies it.
|
13
|
+
# This does not halt the flow of control and is a passive attempt to authenticate only.
|
14
|
+
# :api: public
|
15
|
+
def authenticate(scope)
|
16
|
+
current?(scope) || run_serializers_for(scope) || nil
|
17
|
+
end
|
18
|
+
|
19
|
+
# Same as +authenticate+ except on failure it will trow a
|
20
|
+
# :unauthenticated symbol.
|
21
|
+
# :api: public
|
22
|
+
def authenticate!(scope)
|
23
|
+
unless (resource = authenticate(scope))
|
24
|
+
raise Authenticatable::UnauthenticatedError
|
25
|
+
end
|
26
|
+
|
27
|
+
resource
|
28
|
+
end
|
29
|
+
|
30
|
+
# Add a resource/scope into the @current hash. This method is called after an authentication
|
31
|
+
# strategy has succeeded, but can also be used in tests/debugging to manually sign in a user.
|
32
|
+
#
|
33
|
+
# PLEASE NOTICE that this method DOES NOT perform any authentication strategies.
|
34
|
+
# To authenticate a user, you must use the +authenticate+ method instead.
|
35
|
+
def sign_in(resource, serializer = nil)
|
36
|
+
scope = scope_from_resource(resource)
|
37
|
+
@current[scope] = resource
|
38
|
+
|
39
|
+
unless serializer.nil?
|
40
|
+
klass = initialize_serializer(scope, serializer)
|
41
|
+
klass.store(resource.id)
|
42
|
+
end
|
43
|
+
|
44
|
+
resource
|
45
|
+
end
|
46
|
+
|
47
|
+
# Remove a resource/scope from the @current hash
|
48
|
+
def sign_out(resource, serializer = nil)
|
49
|
+
scope = scope_from_resource(resource)
|
50
|
+
@current[scope] = nil
|
51
|
+
|
52
|
+
unless serializer.nil?
|
53
|
+
klass = initialize_serializer(scope, serializer)
|
54
|
+
klass.purge!
|
55
|
+
end
|
56
|
+
|
57
|
+
true
|
58
|
+
end
|
59
|
+
|
60
|
+
# Provides access to the user object in a given scope for a request.
|
61
|
+
# Will be nil if not logged in or an instance of the resource if logged in.
|
62
|
+
#
|
63
|
+
# Examples:
|
64
|
+
# env['authenticatable'].current?(:user) => #<User>
|
65
|
+
# env['authenticatable'].current?(:admin) => #<Admin>
|
66
|
+
def current?(scope)
|
67
|
+
scope = scope.to_sym # Make sure to always use symbols.
|
68
|
+
@current[scope]
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
# :api: private
|
74
|
+
def run_serializers_for(scope)
|
75
|
+
serializers = %i[session]
|
76
|
+
serializers.each do |name|
|
77
|
+
serializer = initialize_serializer(scope, name)
|
78
|
+
if (record = serializer.fetch)
|
79
|
+
sign_in(record)
|
80
|
+
return record
|
81
|
+
end
|
82
|
+
end
|
83
|
+
nil
|
84
|
+
end
|
85
|
+
|
86
|
+
# Return a constantized class by scope name.
|
87
|
+
def initialize_serializer(scope, name)
|
88
|
+
class_name = name.to_s.classify
|
89
|
+
klass = "Authenticatable::Serializers::#{class_name}".constantize
|
90
|
+
klass.new(@env, scope)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Convert an instance to a scope symbol by
|
94
|
+
# returning the param_key from ActiveModel#model_name
|
95
|
+
# Examples:
|
96
|
+
# scope_from_resource(#<User>) => :user
|
97
|
+
# scope_from_resource(#<Admin>) => :admin
|
98
|
+
def scope_from_resource(resource)
|
99
|
+
scope = resource.model_name.param_key
|
100
|
+
scope.to_sym # Make sure to always use symbols.
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionDispatch
|
4
|
+
module Routing
|
5
|
+
class Mapper
|
6
|
+
def authenticatable(resource, options = {})
|
7
|
+
# Placeholder method for authenticatable generator.
|
8
|
+
scope = Authenticatable.add_scope(resource, options)
|
9
|
+
authenticatable_scope scope do
|
10
|
+
authenticatable_sessions(scope)
|
11
|
+
authenticatable_registrations(scope)
|
12
|
+
authenticatable_passwords(scope)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
protected
|
17
|
+
|
18
|
+
def authenticatable_scope(scope, &block)
|
19
|
+
constraint = lambda do |request|
|
20
|
+
request.env["authenticatable.scope"] = scope
|
21
|
+
true
|
22
|
+
end
|
23
|
+
|
24
|
+
constraints(constraint) do
|
25
|
+
scope scope.path, as: scope.singular_name, &block
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def authenticatable_sessions(scope)
|
30
|
+
return if scope.skip.include? :sessions
|
31
|
+
|
32
|
+
resource :session, only: [], path: "", controller: scope.controllers[:sessions] do
|
33
|
+
get :new, path: scope.path_names[:sign_in], as: :new
|
34
|
+
post :create, path: scope.path_names[:sign_in]
|
35
|
+
match :destroy, path: scope.path_names[:sign_out], as: :destroy,
|
36
|
+
via: Authenticatable.config.allowed_sign_out_methods
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def authenticatable_registrations(scope)
|
41
|
+
return if scope.skip.include? :registrations
|
42
|
+
|
43
|
+
resource :registration, only: [], path: "", controller: scope.controllers[:registrations] do
|
44
|
+
get :new, path: scope.path_names[:sign_up], as: :new
|
45
|
+
post :create, path: scope.path_names[:sign_up]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def authenticatable_passwords(scope)
|
50
|
+
return if scope.skip.include? :passwords
|
51
|
+
|
52
|
+
resource :password, only: [], path: "", controller: scope.controllers[:passwords] do
|
53
|
+
get :new, path: scope.path_names[:forgot_password], as: :new
|
54
|
+
post :create, path: scope.path_names[:forgot_password]
|
55
|
+
get :edit, path: scope.path_names[:reset_password], as: :edit
|
56
|
+
patch :update, path: scope.path_names[:reset_password], as: :update
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/string/inflections"
|
4
|
+
|
5
|
+
# rubocop:disable Layout/EmptyLinesAroundAttributeAccessor
|
6
|
+
module Authenticatable
|
7
|
+
class Scope
|
8
|
+
# The resource name added by the authenticatable-route (for example :users)
|
9
|
+
attr_accessor :name
|
10
|
+
@name = nil
|
11
|
+
|
12
|
+
# The resource name converted to singluar (used by for example url helpers)
|
13
|
+
# authenticatable :users generates helper methods like:
|
14
|
+
# new_user_session_path and user_signed_in?
|
15
|
+
attr_accessor :singular_name
|
16
|
+
@singular_name = nil
|
17
|
+
|
18
|
+
# The resource name converted to plural (used to generate controllers etc). This is used
|
19
|
+
# as a fallback in case the user defines a resource in plural like:
|
20
|
+
# authenticatable :user
|
21
|
+
attr_accessor :plural_name
|
22
|
+
@plural_name = nil
|
23
|
+
|
24
|
+
# You can customize a resource path to something other than the default,
|
25
|
+
# including the possibility to change path for I18n:
|
26
|
+
# authenticatable :users, { path: I18n.translate('routes.account') }
|
27
|
+
attr_accessor :path
|
28
|
+
@path = {}
|
29
|
+
|
30
|
+
# You can customize a resource's path_names to something other than the default,
|
31
|
+
# including the possibility to change path names for I18n:
|
32
|
+
# authenticatable :users, { path_names: { sign_in: I18n.translate('routes.login') } }
|
33
|
+
attr_accessor :path_names
|
34
|
+
@path_names = {}
|
35
|
+
|
36
|
+
# You can also specify another controller than the default one in case you
|
37
|
+
# want to override some controller actions.
|
38
|
+
attr_accessor :controllers
|
39
|
+
@controllers = {}
|
40
|
+
|
41
|
+
# You can also specify controllers you want to skip. For example if you
|
42
|
+
# want to disable registrations for an Admin resource.
|
43
|
+
attr_accessor :skip
|
44
|
+
@skip = {}
|
45
|
+
|
46
|
+
# Create a new Scope resource that can be added to Authenticatable.scopes
|
47
|
+
def initialize(resource_name, options = {})
|
48
|
+
@name = resource_name.to_s
|
49
|
+
@singular_name = ActiveSupport::Inflector.singularize(resource_name)
|
50
|
+
@plural_name = ActiveSupport::Inflector.pluralize(resource_name)
|
51
|
+
@path = options[:path] || default_path
|
52
|
+
@path_names = default_path_names.merge(options[:path_names] || {})
|
53
|
+
@controllers = default_controllers.merge(options[:controllers] || {})
|
54
|
+
@skip = *options[:skip]
|
55
|
+
@skip = @skip.map(&:to_sym)
|
56
|
+
end
|
57
|
+
|
58
|
+
def classify
|
59
|
+
ActiveSupport::Inflector.classify @singular_name
|
60
|
+
end
|
61
|
+
|
62
|
+
def klass
|
63
|
+
classify.constantize
|
64
|
+
end
|
65
|
+
|
66
|
+
# Create magic predicates for verifying that a
|
67
|
+
# controller exists and hasn't been skipped for
|
68
|
+
# the current scope in routes.rb.
|
69
|
+
#
|
70
|
+
# Example
|
71
|
+
# current_scope.registrations? => false
|
72
|
+
# when registrations are skipped in routes:
|
73
|
+
# authenticatable :admins, skip: [:registrations]
|
74
|
+
#
|
75
|
+
# rubocop:disable Style/MissingRespondToMissing
|
76
|
+
def method_missing(method_name)
|
77
|
+
@controllers.each do |controller, _routes|
|
78
|
+
return @skip.exclude?(controller) if method_name.to_s == "#{controller}?"
|
79
|
+
end
|
80
|
+
|
81
|
+
super # return NoMethodError
|
82
|
+
end
|
83
|
+
# rubocop:enable Style/MissingRespondToMissing
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def default_path
|
88
|
+
@plural_name
|
89
|
+
end
|
90
|
+
|
91
|
+
def default_path_names
|
92
|
+
{
|
93
|
+
sign_in: "sign_in",
|
94
|
+
sign_up: "sign_up",
|
95
|
+
sign_out: "sign_out",
|
96
|
+
forgot_password: "forgot_password",
|
97
|
+
reset_password: "reset_password"
|
98
|
+
}
|
99
|
+
end
|
100
|
+
|
101
|
+
def default_controllers
|
102
|
+
{
|
103
|
+
sessions: "authenticatable/sessions",
|
104
|
+
registrations: "authenticatable/registrations",
|
105
|
+
passwords: "authenticatable/passwords"
|
106
|
+
}
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
# rubocop:enable Layout/EmptyLinesAroundAttributeAccessor
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Authenticatable
|
4
|
+
module Serializers
|
5
|
+
class Base
|
6
|
+
# :api: public
|
7
|
+
attr_accessor :env, :scope, :record
|
8
|
+
|
9
|
+
# Initialize and prepare variables.
|
10
|
+
def initialize(env, scope)
|
11
|
+
@env = env
|
12
|
+
@scope = scope
|
13
|
+
@record = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
# Access scope settings for current scope that was set in routes.rb
|
17
|
+
def current_scope
|
18
|
+
Authenticatable.scopes[scope.to_sym]
|
19
|
+
end
|
20
|
+
|
21
|
+
# Access the class for the current scope.
|
22
|
+
# For example if the current scope is "user":
|
23
|
+
# resource_class.find(:id) == User.find(:id)
|
24
|
+
def resource_class
|
25
|
+
current_scope.klass
|
26
|
+
end
|
27
|
+
|
28
|
+
# Convenience access the rack request.
|
29
|
+
def request
|
30
|
+
@request ||= Rack::Request.new(@env)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Delegate #params to request#params.
|
34
|
+
# Example:
|
35
|
+
# params["foo"] == request.params["foo"]
|
36
|
+
delegate :params, to: :request
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Authenticatable
|
4
|
+
module Serializers
|
5
|
+
class Session < Authenticatable::Serializers::Base
|
6
|
+
# Fetch record from Rack session.
|
7
|
+
# Example:
|
8
|
+
# serializer.fetch(:user) => <#User>
|
9
|
+
def fetch
|
10
|
+
return nil unless (record_id = request.session[session_key])
|
11
|
+
|
12
|
+
resource_class.find_by(id: record_id)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Store record id in Rack session.
|
16
|
+
# Usage:
|
17
|
+
# serializer.store(@resource)
|
18
|
+
def store(id)
|
19
|
+
request.session[session_key] = id
|
20
|
+
end
|
21
|
+
|
22
|
+
# Delete record id from Rack session
|
23
|
+
# Usage:
|
24
|
+
# serializer.purge
|
25
|
+
def purge!
|
26
|
+
request.session.delete(session_key)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def session_key
|
32
|
+
:"authenticatable_#{@scope}_id"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|