authenticatable 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.md +19 -0
  3. data/README.md +144 -0
  4. data/app/controllers/authenticatable/passwords_controller.rb +51 -0
  5. data/app/controllers/authenticatable/registrations_controller.rb +28 -0
  6. data/app/controllers/authenticatable/sessions_controller.rb +49 -0
  7. data/app/controllers/authenticatable_controller.rb +101 -0
  8. data/app/mailers/authenticatable/mailer.rb +17 -0
  9. data/app/views/authenticatable/mailer/reset_password.html.erb +8 -0
  10. data/app/views/authenticatable/passwords/_edit_form.html.erb +6 -0
  11. data/app/views/authenticatable/passwords/_new_form.html.erb +6 -0
  12. data/app/views/authenticatable/passwords/edit.html.erb +7 -0
  13. data/app/views/authenticatable/passwords/new.html.erb +7 -0
  14. data/app/views/authenticatable/registrations/_form.html.erb +12 -0
  15. data/app/views/authenticatable/registrations/new.html.erb +5 -0
  16. data/app/views/authenticatable/sessions/_form.html.erb +5 -0
  17. data/app/views/authenticatable/sessions/new.html.erb +7 -0
  18. data/app/views/authenticatable/shared/_errors.html.erb +7 -0
  19. data/app/views/authenticatable/shared/_flash_messages.html.erb +3 -0
  20. data/app/views/authenticatable/shared/_links.html.erb +12 -0
  21. data/config/locales/en.yml +14 -0
  22. data/lib/authenticatable/controllers/helpers.rb +72 -0
  23. data/lib/authenticatable/controllers/url_helpers.rb +67 -0
  24. data/lib/authenticatable/controllers.rb +9 -0
  25. data/lib/authenticatable/engine.rb +17 -0
  26. data/lib/authenticatable/errors/unauthenticated_error.rb +6 -0
  27. data/lib/authenticatable/errors.rb +6 -0
  28. data/lib/authenticatable/manager.rb +16 -0
  29. data/lib/authenticatable/models/email_validator.rb +17 -0
  30. data/lib/authenticatable/models/identifier.rb +43 -0
  31. data/lib/authenticatable/models/password.rb +73 -0
  32. data/lib/authenticatable/models.rb +67 -0
  33. data/lib/authenticatable/proxy.rb +103 -0
  34. data/lib/authenticatable/rails/routes.rb +61 -0
  35. data/lib/authenticatable/rspec.rb +8 -0
  36. data/lib/authenticatable/scope.rb +110 -0
  37. data/lib/authenticatable/serializers/base.rb +39 -0
  38. data/lib/authenticatable/serializers/session.rb +36 -0
  39. data/lib/authenticatable/serializers.rb +9 -0
  40. data/lib/authenticatable/testing/controller_helpers.rb +31 -0
  41. data/lib/authenticatable/token.rb +13 -0
  42. data/lib/authenticatable/version.rb +5 -0
  43. data/lib/authenticatable.rb +100 -0
  44. data/lib/generators/active_record/authenticatable_generator.rb +63 -0
  45. data/lib/generators/active_record/templates/migration.tt +15 -0
  46. data/lib/generators/active_record/templates/migration_existing.tt +23 -0
  47. data/lib/generators/authenticatable/authenticatable_generator.rb +18 -0
  48. data/lib/generators/authenticatable/orm_helpers.rb +30 -0
  49. data/lib/generators/authenticatable/views_generator.rb +19 -0
  50. 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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "authenticatable/controllers/url_helpers"
4
+ require "authenticatable/controllers/helpers"
5
+
6
+ module Authenticatable
7
+ module Controllers
8
+ end
9
+ 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,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Authenticatable
4
+ class UnauthenticatedError < StandardError
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "authenticatable/errors/unauthenticated_error"
4
+
5
+ module Authenticatable
6
+ 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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rspec/rails"
4
+ require "authenticatable/testing/controller_helpers"
5
+
6
+ RSpec.configure do |config|
7
+ config.include Authenticatable::Testing::ControllerHelpers, type: :controller
8
+ 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