rodauth-rails 0.9.0 → 0.13.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 +44 -0
  3. data/README.md +417 -250
  4. data/lib/generators/rodauth/install_generator.rb +17 -0
  5. data/lib/generators/rodauth/migration/base.erb +2 -2
  6. data/lib/generators/rodauth/templates/app/lib/rodauth_app.rb +31 -29
  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 +32 -4
  25. data/lib/rodauth/rails/app.rb +20 -22
  26. data/lib/rodauth/rails/app/flash.rb +2 -8
  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 -210
  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 +12 -6
  40. data/lib/generators/rodauth/mailer_generator.rb +0 -37
@@ -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,214 +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
- def rails_controller
196
- if only_json? && Rodauth::Rails.api_only?
197
- ActionController::API
198
- else
199
- ActionController::Base
200
- end
201
- end
202
-
203
- # ActionMailer subclass for correct email delivering.
204
- class Mailer < ActionMailer::Base
205
- def create_email(**options)
206
- mail(**options)
207
- end
208
- 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
209
20
  end
210
-
211
- # Assign feature and feature configuration to constants for introspection.
212
- Rails::Feature = FEATURES[:rails]
213
- Rails::FeatureConfiguration = FEATURES[:rails].configuration
214
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
@@ -0,0 +1,61 @@
1
+ module Rodauth
2
+ module Rails
3
+ module Feature
4
+ module Callbacks
5
+ private
6
+
7
+ # Runs controller callbacks and rescue handlers around Rodauth actions.
8
+ def _around_rodauth(&block)
9
+ result = nil
10
+
11
+ rails_controller_rescue do
12
+ rails_controller_callbacks do
13
+ result = catch(:halt) { super(&block) }
14
+ end
15
+ end
16
+
17
+ result = handle_rails_controller_response(result)
18
+
19
+ throw :halt, result if result
20
+ end
21
+
22
+ # Runs any #(before|around|after)_action controller callbacks.
23
+ def rails_controller_callbacks(&block)
24
+ rails_controller_instance.run_callbacks(:process_action, &block)
25
+ end
26
+
27
+ # Runs any registered #rescue_from controller handlers.
28
+ def rails_controller_rescue
29
+ yield
30
+ rescue Exception => exception
31
+ rails_controller_instance.rescue_with_handler(exception) || raise
32
+
33
+ unless rails_controller_instance.performed?
34
+ raise Rodauth::Rails::Error, "rescue_from handler didn't write any response"
35
+ end
36
+ end
37
+
38
+ # Handles controller rendering a response or setting response headers.
39
+ def handle_rails_controller_response(result)
40
+ if rails_controller_instance.performed?
41
+ rails_controller_response
42
+ elsif result
43
+ result[1].merge!(rails_controller_instance.response.headers)
44
+ result
45
+ end
46
+ end
47
+
48
+ # Returns Roda response from controller response if set.
49
+ def rails_controller_response
50
+ controller_response = rails_controller_instance.response
51
+
52
+ response.status = controller_response.status
53
+ response.headers.merge! controller_response.headers
54
+ response.write controller_response.body
55
+
56
+ response.finish
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end