authenticatable 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|