rodauth-rails 0.8.2 → 0.12.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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +52 -0
  3. data/README.md +453 -223
  4. data/lib/generators/rodauth/install_generator.rb +26 -15
  5. data/lib/generators/rodauth/migration/base.erb +2 -2
  6. data/lib/generators/rodauth/templates/app/lib/rodauth_app.rb +50 -49
  7. data/lib/generators/rodauth/templates/app/mailers/rodauth_mailer.rb +3 -3
  8. data/lib/generators/rodauth/templates/app/views/rodauth/_global_logout_field.html.erb +1 -1
  9. data/lib/generators/rodauth/templates/app/views/rodauth/_login_confirm_field.html.erb +2 -2
  10. data/lib/generators/rodauth/templates/app/views/rodauth/_login_display.html.erb +2 -2
  11. data/lib/generators/rodauth/templates/app/views/rodauth/_login_field.html.erb +2 -2
  12. data/lib/generators/rodauth/templates/app/views/rodauth/_new_password_field.html.erb +2 -2
  13. data/lib/generators/rodauth/templates/app/views/rodauth/_otp_auth_code_field.html.erb +2 -2
  14. data/lib/generators/rodauth/templates/app/views/rodauth/_password_confirm_field.html.erb +2 -2
  15. data/lib/generators/rodauth/templates/app/views/rodauth/_password_field.html.erb +2 -2
  16. data/lib/generators/rodauth/templates/app/views/rodauth/_recovery_code_field.html.erb +2 -2
  17. data/lib/generators/rodauth/templates/app/views/rodauth/_sms_code_field.html.erb +2 -2
  18. data/lib/generators/rodauth/templates/app/views/rodauth/_sms_phone_field.html.erb +2 -2
  19. data/lib/generators/rodauth/templates/app/views/rodauth/_submit.html.erb +1 -1
  20. data/lib/generators/rodauth/templates/app/views/rodauth/otp_setup.html.erb +2 -2
  21. data/lib/generators/rodauth/templates/app/views/rodauth/remember.html.erb +1 -1
  22. data/lib/generators/rodauth/templates/app/views/rodauth/webauthn_remove.html.erb +1 -1
  23. data/lib/generators/rodauth/templates/app/views/rodauth_mailer/unlock_account.text.erb +1 -1
  24. data/lib/rodauth/rails.rb +20 -0
  25. data/lib/rodauth/rails/app.rb +23 -31
  26. data/lib/rodauth/rails/app/flash.rb +7 -11
  27. data/lib/rodauth/rails/app/middleware.rb +20 -10
  28. data/lib/rodauth/rails/auth.rb +40 -0
  29. data/lib/rodauth/rails/controller_methods.rb +1 -5
  30. data/lib/rodauth/rails/feature.rb +17 -202
  31. data/lib/rodauth/rails/feature/base.rb +62 -0
  32. data/lib/rodauth/rails/feature/callbacks.rb +61 -0
  33. data/lib/rodauth/rails/feature/csrf.rb +65 -0
  34. data/lib/rodauth/rails/feature/email.rb +30 -0
  35. data/lib/rodauth/rails/feature/instrumentation.rb +71 -0
  36. data/lib/rodauth/rails/feature/render.rb +41 -0
  37. data/lib/rodauth/rails/version.rb +1 -1
  38. data/rodauth-rails.gemspec +1 -1
  39. metadata +15 -9
  40. data/lib/generators/rodauth/mailer_generator.rb +0 -37
@@ -1,6 +1,5 @@
1
1
  require "roda"
2
- require "rodauth"
3
- require "rodauth/rails/feature"
2
+ require "rodauth/rails/auth"
4
3
 
5
4
  module Rodauth
6
5
  module Rails
@@ -11,46 +10,39 @@ module Rodauth
11
10
 
12
11
  plugin :hooks
13
12
  plugin :render, layout: false
13
+ plugin :pass
14
14
 
15
- def self.configure(name = nil, **options, &block)
16
- unless options[:json] == :only
17
- require "rodauth/rails/app/flash"
18
- plugin Flash
19
- end
20
-
21
- plugin :rodauth, name: name, csrf: false, flash: false, **options do
22
- # load the Rails integration
23
- enable :rails
24
-
25
- if options[:json] == :only && ActionPack.version >= Gem::Version.new("5.0")
26
- rails_controller { ActionController::API }
27
- else
28
- rails_controller { ActionController::Base }
29
- end
15
+ unless Rodauth::Rails.api_only?
16
+ require "rodauth/rails/app/flash"
17
+ plugin Flash
18
+ end
30
19
 
31
- # database functions are more complex to set up, so disable them by default
32
- use_database_authentication_functions? false
20
+ def self.configure(*args, **options, &block)
21
+ auth_class = args.shift if args[0].is_a?(Class)
22
+ name = args.shift if args[0].is_a?(Symbol)
33
23
 
34
- # avoid having to set deadline values in column default values
35
- set_deadline_values? true
24
+ fail ArgumentError, "need to pass optional Rodauth::Auth subclass and optional configuration name" if args.any?
36
25
 
37
- # use HMACs for additional security
38
- hmac_secret { Rodauth::Rails.secret_key_base }
26
+ auth_class ||= Class.new(Rodauth::Rails::Auth)
39
27
 
40
- # evaluate user configuration
41
- instance_exec(&block)
28
+ plugin :rodauth, auth_class: auth_class, name: name, csrf: false, flash: false, json: true, **options do
29
+ instance_exec(&block) if block
42
30
  end
43
31
  end
44
32
 
45
33
  before do
46
- (opts[:rodauths] || {}).each do |name, _|
47
- if name
48
- env["rodauth.#{name}"] = rodauth(name)
49
- else
50
- env["rodauth"] = rodauth
51
- end
34
+ opts[:rodauths]&.each_key do |name|
35
+ env[["rodauth", *name].join(".")] = rodauth(name)
52
36
  end
53
37
  end
38
+
39
+ def rails_routes
40
+ ::Rails.application.routes.url_helpers
41
+ end
42
+
43
+ def rails_request
44
+ ActionDispatch::Request.new(env)
45
+ end
54
46
  end
55
47
  end
56
48
  end
@@ -27,22 +27,18 @@ module Rodauth
27
27
  end
28
28
 
29
29
  def flash
30
- rails_request.flash
30
+ scope.rails_request.flash
31
31
  end
32
32
 
33
- def commit_flash
34
- if ActionPack.version >= Gem::Version.new("5.0")
35
- rails_request.commit_flash
36
- else
33
+ if ActionPack.version >= Gem::Version.new("5.0")
34
+ def commit_flash
35
+ scope.rails_request.commit_flash
36
+ end
37
+ else
38
+ def commit_flash
37
39
  # ActionPack 4.2 automatically commits flash
38
40
  end
39
41
  end
40
-
41
- private
42
-
43
- def rails_request
44
- ActionDispatch::Request.new(env)
45
- end
46
42
  end
47
43
  end
48
44
  end
@@ -3,20 +3,30 @@ module Rodauth
3
3
  class App
4
4
  # Roda plugin that extends middleware plugin by propagating response headers.
5
5
  module Middleware
6
- def self.load_dependencies(app)
7
- app.plugin :hooks
8
- end
9
-
10
6
  def self.configure(app)
11
- app.after do
12
- if response.empty? && response.headers.any?
13
- env["rodauth.rails.headers"] = response.headers
7
+ handle_result = -> (env, res) do
8
+ if headers = env.delete("rodauth.rails.headers")
9
+ res[1] = headers.merge(res[1])
14
10
  end
15
11
  end
16
12
 
17
- app.plugin :middleware, handle_result: -> (env, res) do
18
- if headers = env.delete("rodauth.rails.headers")
19
- res[1] = headers.merge(res[1])
13
+ app.plugin :middleware, handle_result: handle_result do |middleware|
14
+ middleware.plugin :hooks
15
+
16
+ middleware.after do
17
+ if response.empty? && response.headers.any?
18
+ env["rodauth.rails.headers"] = response.headers
19
+ end
20
+ end
21
+
22
+ middleware.class_eval do
23
+ def self.inspect
24
+ "#{superclass}::Middleware"
25
+ end
26
+
27
+ def inspect
28
+ "#<#{self.class.inspect} request=#{request.inspect} response=#{response.inspect}>"
29
+ end
20
30
  end
21
31
  end
22
32
  end
@@ -0,0 +1,40 @@
1
+ require "rodauth"
2
+ require "rodauth/rails/feature"
3
+
4
+ module Rodauth
5
+ module Rails
6
+ # Base auth class that applies some default configuration and supports
7
+ # multi-level inheritance.
8
+ class Auth < Rodauth::Auth
9
+ class << self
10
+ attr_writer :features
11
+ attr_writer :routes
12
+ attr_accessor :configuration
13
+ end
14
+
15
+ def self.inherited(auth_class)
16
+ super
17
+ auth_class.roda_class = Rodauth::Rails.app
18
+ auth_class.features = features.dup
19
+ auth_class.routes = routes.dup
20
+ auth_class.route_hash = route_hash.dup
21
+ auth_class.configuration = configuration.clone
22
+ auth_class.configuration.instance_variable_set(:@auth, auth_class)
23
+ end
24
+
25
+ # apply default configuration
26
+ configure do
27
+ enable :rails
28
+
29
+ # database functions are more complex to set up, so disable them by default
30
+ use_database_authentication_functions? false
31
+
32
+ # avoid having to set deadline values in column default values
33
+ set_deadline_values? true
34
+
35
+ # use HMACs for additional security
36
+ hmac_secret { Rodauth::Rails.secret_key_base }
37
+ end
38
+ end
39
+ end
40
+ end
@@ -9,11 +9,7 @@ module Rodauth
9
9
  end
10
10
 
11
11
  def rodauth(name = nil)
12
- if name
13
- request.env["rodauth.#{name}"]
14
- else
15
- request.env["rodauth"]
16
- end
12
+ request.env.fetch ["rodauth", *name].join(".")
17
13
  end
18
14
  end
19
15
  end
@@ -1,206 +1,21 @@
1
1
  module Rodauth
2
2
  Feature.define(:rails) do
3
- depends :email_base
4
-
5
- # List of overridable methods.
6
- auth_methods(
7
- :rails_render,
8
- :rails_csrf_tag,
9
- :rails_csrf_param,
10
- :rails_csrf_token,
11
- :rails_check_csrf!,
12
- :rails_controller,
13
- )
14
-
15
- auth_cached_method :rails_controller_instance
16
-
17
- # Renders templates with layout. First tries to render a user-defined
18
- # template, otherwise falls back to Rodauth's template.
19
- def view(page, *)
20
- rails_render(action: page.tr("-", "_"), layout: true) ||
21
- rails_render(html: super.html_safe, layout: true)
22
- end
23
-
24
- # Renders templates without layout. First tries to render a user-defined
25
- # template or partial, otherwise falls back to Rodauth's template.
26
- def render(page)
27
- rails_render(partial: page.tr("-", "_"), layout: false) ||
28
- rails_render(action: page.tr("-", "_"), layout: false) ||
29
- super.html_safe
30
- end
31
-
32
- # Render Rails CSRF tags in Rodauth templates.
33
- def csrf_tag(*)
34
- rails_csrf_tag
35
- end
36
-
37
- # Verify Rails' authenticity token.
38
- def check_csrf
39
- rails_check_csrf!
40
- end
41
-
42
- # Have Rodauth call #check_csrf automatically.
43
- def check_csrf?
44
- true
45
- end
46
-
47
- # Reset Rails session to protect from session fixation attacks.
48
- def clear_session
49
- rails_controller_instance.reset_session
50
- end
51
-
52
- # Default the flash error key to Rails' default :alert.
53
- def flash_error_key
54
- :alert
55
- end
56
-
57
- # Evaluates the block in context of a Rodauth controller instance.
58
- def rails_controller_eval(&block)
59
- rails_controller_instance.instance_exec(&block)
60
- end
61
-
62
- def button(*)
63
- super.html_safe
64
- end
65
-
66
- private
67
-
68
- # Runs controller callbacks and rescue handlers around Rodauth actions.
69
- def _around_rodauth(&block)
70
- result = nil
71
-
72
- rails_controller_rescue do
73
- rails_controller_callbacks do
74
- result = catch(:halt) { super(&block) }
75
- end
76
- end
77
-
78
- if rails_controller_instance.performed?
79
- rails_controller_response
80
- elsif result
81
- result[1].merge!(rails_controller_instance.response.headers)
82
- throw :halt, result
83
- else
84
- result
85
- end
86
- end
87
-
88
- # Runs any #(before|around|after)_action controller callbacks.
89
- def rails_controller_callbacks
90
- # don't verify CSRF token as part of callbacks, Rodauth will do that
91
- rails_controller_forgery_protection { false }
92
-
93
- rails_controller_instance.run_callbacks(:process_action) do
94
- # turn the setting back to default so that form tags generate CSRF tags
95
- rails_controller_forgery_protection { rails_controller.allow_forgery_protection }
96
-
97
- yield
98
- end
99
- end
100
-
101
- # Runs any registered #rescue_from controller handlers.
102
- def rails_controller_rescue
103
- yield
104
- rescue Exception => exception
105
- rails_controller_instance.rescue_with_handler(exception) || raise
106
-
107
- unless rails_controller_instance.performed?
108
- raise Rodauth::Rails::Error, "rescue_from handler didn't write any response"
109
- end
110
- end
111
-
112
- # Returns Roda response from controller response if set.
113
- def rails_controller_response
114
- controller_response = rails_controller_instance.response
115
-
116
- response.status = controller_response.status
117
- response.headers.merge! controller_response.headers
118
- response.write controller_response.body
119
-
120
- request.halt
121
- end
122
-
123
- # Create emails with ActionMailer which uses configured delivery method.
124
- def create_email_to(to, subject, body)
125
- Mailer.create_email(to: to, from: email_from, subject: "#{email_subject_prefix}#{subject}", body: body)
126
- end
127
-
128
- # Delivers the given email.
129
- def send_email(email)
130
- email.deliver_now
131
- end
132
-
133
- # Calls the Rails renderer, returning nil if a template is missing.
134
- def rails_render(*args)
135
- return if rails_api_controller?
136
-
137
- rails_controller_instance.render_to_string(*args)
138
- rescue ActionView::MissingTemplate
139
- nil
140
- end
141
-
142
- # Calls the controller to verify the authenticity token.
143
- def rails_check_csrf!
144
- rails_controller_instance.send(:verify_authenticity_token)
145
- end
146
-
147
- # Hidden tag with Rails CSRF token inserted into Rodauth templates.
148
- def rails_csrf_tag
149
- %(<input type="hidden" name="#{rails_csrf_param}" value="#{rails_csrf_token}">)
150
- end
151
-
152
- # The request parameter under which to send the Rails CSRF token.
153
- def rails_csrf_param
154
- rails_controller.request_forgery_protection_token
155
- end
156
-
157
- # The Rails CSRF token value inserted into Rodauth templates.
158
- def rails_csrf_token
159
- rails_controller_instance.send(:form_authenticity_token)
160
- end
161
-
162
- # allows/disables forgery protection
163
- def rails_controller_forgery_protection(&value)
164
- return if rails_api_controller?
165
-
166
- rails_controller_instance.allow_forgery_protection = value.call
167
- end
168
-
169
- # Instances of the configured controller with current request's env hash.
170
- def _rails_controller_instance
171
- controller = rails_controller.new
172
- rails_request = ActionDispatch::Request.new(scope.env)
173
-
174
- prepare_rails_controller(controller, rails_request)
175
-
176
- controller
177
- end
178
-
179
- if ActionPack.version >= Gem::Version.new("5.0")
180
- def prepare_rails_controller(controller, rails_request)
181
- controller.set_request! rails_request
182
- controller.set_response! rails_controller.make_response!(rails_request)
183
- end
184
- else
185
- def prepare_rails_controller(controller, rails_request)
186
- controller.send(:set_response!, rails_request)
187
- controller.instance_variable_set(:@_request, rails_request)
188
- end
189
- end
190
-
191
- def rails_api_controller?
192
- defined?(ActionController::API) && rails_controller <= ActionController::API
193
- end
194
-
195
- # ActionMailer subclass for correct email delivering.
196
- class Mailer < ActionMailer::Base
197
- def create_email(**options)
198
- mail(**options)
199
- end
200
- end
3
+ # Assign feature and feature configuration to constants for introspection.
4
+ Rodauth::Rails::Feature = self
5
+ Rodauth::Rails::FeatureConfiguration = self.configuration
6
+
7
+ require "rodauth/rails/feature/base"
8
+ require "rodauth/rails/feature/callbacks"
9
+ require "rodauth/rails/feature/csrf"
10
+ require "rodauth/rails/feature/render"
11
+ require "rodauth/rails/feature/email"
12
+ require "rodauth/rails/feature/instrumentation"
13
+
14
+ include Rodauth::Rails::Feature::Base
15
+ include Rodauth::Rails::Feature::Callbacks
16
+ include Rodauth::Rails::Feature::Csrf
17
+ include Rodauth::Rails::Feature::Render
18
+ include Rodauth::Rails::Feature::Email
19
+ include Rodauth::Rails::Feature::Instrumentation
201
20
  end
202
-
203
- # Assign feature and feature configuration to constants for introspection.
204
- Rails::Feature = FEATURES[:rails]
205
- Rails::FeatureConfiguration = FEATURES[:rails].configuration
206
21
  end
@@ -0,0 +1,62 @@
1
+ module Rodauth
2
+ module Rails
3
+ module Feature
4
+ module Base
5
+ def self.included(feature)
6
+ feature.auth_methods :rails_controller
7
+ feature.auth_cached_method :rails_controller_instance
8
+ end
9
+
10
+ # Reset Rails session to protect from session fixation attacks.
11
+ def clear_session
12
+ rails_controller_instance.reset_session
13
+ end
14
+
15
+ # Default the flash error key to Rails' default :alert.
16
+ def flash_error_key
17
+ :alert
18
+ end
19
+
20
+ # Evaluates the block in context of a Rodauth controller instance.
21
+ def rails_controller_eval(&block)
22
+ rails_controller_instance.instance_exec(&block)
23
+ end
24
+
25
+ delegate :rails_routes, :rails_request, to: :scope
26
+
27
+ private
28
+
29
+ # Instances of the configured controller with current request's env hash.
30
+ def _rails_controller_instance
31
+ controller = rails_controller.new
32
+ prepare_rails_controller(controller, rails_request)
33
+ controller
34
+ end
35
+
36
+ if ActionPack.version >= Gem::Version.new("5.0")
37
+ def prepare_rails_controller(controller, rails_request)
38
+ controller.set_request! rails_request
39
+ controller.set_response! rails_controller.make_response!(rails_request)
40
+ end
41
+ else
42
+ def prepare_rails_controller(controller, rails_request)
43
+ controller.send(:set_response!, rails_request)
44
+ controller.instance_variable_set(:@_request, rails_request)
45
+ end
46
+ end
47
+
48
+ def rails_api_controller?
49
+ defined?(ActionController::API) && rails_controller <= ActionController::API
50
+ end
51
+
52
+ def rails_controller
53
+ if only_json? && Rodauth::Rails.api_only?
54
+ ActionController::API
55
+ else
56
+ ActionController::Base
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end