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.
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