rodauth-rails 0.11.0 → 0.15.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 (76) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -0
  3. data/README.md +316 -218
  4. data/lib/generators/rodauth/templates/app/lib/rodauth_app.rb +8 -4
  5. data/lib/generators/rodauth/templates/app/models/account.rb +1 -0
  6. data/lib/generators/rodauth/templates/app/views/rodauth/_email_auth_request_form.html.erb +1 -1
  7. data/lib/generators/rodauth/templates/app/views/rodauth/_field.html.erb +2 -2
  8. data/lib/generators/rodauth/templates/app/views/rodauth/_field_error.html.erb +2 -2
  9. data/lib/generators/rodauth/templates/app/views/rodauth/_global_logout_field.html.erb +2 -2
  10. data/lib/generators/rodauth/templates/app/views/rodauth/_login_confirm_field.html.erb +3 -3
  11. data/lib/generators/rodauth/templates/app/views/rodauth/_login_display.html.erb +3 -3
  12. data/lib/generators/rodauth/templates/app/views/rodauth/_login_field.html.erb +3 -3
  13. data/lib/generators/rodauth/templates/app/views/rodauth/_login_form.html.erb +3 -3
  14. data/lib/generators/rodauth/templates/app/views/rodauth/_login_form_footer.html.erb +2 -2
  15. data/lib/generators/rodauth/templates/app/views/rodauth/_login_form_header.html.erb +2 -2
  16. data/lib/generators/rodauth/templates/app/views/rodauth/_login_hidden_field.html.erb +1 -1
  17. data/lib/generators/rodauth/templates/app/views/rodauth/_new_password_field.html.erb +3 -3
  18. data/lib/generators/rodauth/templates/app/views/rodauth/_otp_auth_code_field.html.erb +3 -3
  19. data/lib/generators/rodauth/templates/app/views/rodauth/_password_confirm_field.html.erb +3 -3
  20. data/lib/generators/rodauth/templates/app/views/rodauth/_password_field.html.erb +3 -3
  21. data/lib/generators/rodauth/templates/app/views/rodauth/_recovery_code_field.html.erb +3 -3
  22. data/lib/generators/rodauth/templates/app/views/rodauth/_recovery_codes_form.html.erb +4 -4
  23. data/lib/generators/rodauth/templates/app/views/rodauth/_sms_code_field.html.erb +3 -3
  24. data/lib/generators/rodauth/templates/app/views/rodauth/_sms_phone_field.html.erb +3 -3
  25. data/lib/generators/rodauth/templates/app/views/rodauth/_submit.html.erb +1 -1
  26. data/lib/generators/rodauth/templates/app/views/rodauth/add_recovery_codes.html.erb +2 -2
  27. data/lib/generators/rodauth/templates/app/views/rodauth/change_login.html.erb +3 -3
  28. data/lib/generators/rodauth/templates/app/views/rodauth/change_password.html.erb +3 -3
  29. data/lib/generators/rodauth/templates/app/views/rodauth/close_account.html.erb +2 -2
  30. data/lib/generators/rodauth/templates/app/views/rodauth/confirm_password.html.erb +1 -1
  31. data/lib/generators/rodauth/templates/app/views/rodauth/create_account.html.erb +4 -4
  32. data/lib/generators/rodauth/templates/app/views/rodauth/email_auth.html.erb +1 -1
  33. data/lib/generators/rodauth/templates/app/views/rodauth/logout.html.erb +2 -2
  34. data/lib/generators/rodauth/templates/app/views/rodauth/multi_phase_login.html.erb +1 -1
  35. data/lib/generators/rodauth/templates/app/views/rodauth/otp_auth.html.erb +1 -1
  36. data/lib/generators/rodauth/templates/app/views/rodauth/otp_disable.html.erb +2 -2
  37. data/lib/generators/rodauth/templates/app/views/rodauth/otp_setup.html.erb +9 -9
  38. data/lib/generators/rodauth/templates/app/views/rodauth/recovery_auth.html.erb +1 -1
  39. data/lib/generators/rodauth/templates/app/views/rodauth/remember.html.erb +5 -5
  40. data/lib/generators/rodauth/templates/app/views/rodauth/reset_password.html.erb +2 -2
  41. data/lib/generators/rodauth/templates/app/views/rodauth/reset_password_request.html.erb +2 -2
  42. data/lib/generators/rodauth/templates/app/views/rodauth/sms_auth.html.erb +1 -1
  43. data/lib/generators/rodauth/templates/app/views/rodauth/sms_confirm.html.erb +1 -1
  44. data/lib/generators/rodauth/templates/app/views/rodauth/sms_disable.html.erb +2 -2
  45. data/lib/generators/rodauth/templates/app/views/rodauth/sms_request.html.erb +1 -1
  46. data/lib/generators/rodauth/templates/app/views/rodauth/sms_setup.html.erb +2 -2
  47. data/lib/generators/rodauth/templates/app/views/rodauth/two_factor_auth.html.erb +1 -1
  48. data/lib/generators/rodauth/templates/app/views/rodauth/two_factor_disable.html.erb +2 -2
  49. data/lib/generators/rodauth/templates/app/views/rodauth/two_factor_manage.html.erb +6 -6
  50. data/lib/generators/rodauth/templates/app/views/rodauth/unlock_account.html.erb +2 -2
  51. data/lib/generators/rodauth/templates/app/views/rodauth/unlock_account_request.html.erb +1 -1
  52. data/lib/generators/rodauth/templates/app/views/rodauth/verify_account.html.erb +3 -3
  53. data/lib/generators/rodauth/templates/app/views/rodauth/verify_account_resend.html.erb +2 -2
  54. data/lib/generators/rodauth/templates/app/views/rodauth/verify_login_change.html.erb +1 -1
  55. data/lib/generators/rodauth/templates/app/views/rodauth/webauthn_auth.html.erb +7 -7
  56. data/lib/generators/rodauth/templates/app/views/rodauth/webauthn_remove.html.erb +6 -6
  57. data/lib/generators/rodauth/templates/app/views/rodauth/webauthn_setup.html.erb +7 -7
  58. data/lib/generators/rodauth/views_generator.rb +26 -4
  59. data/lib/rodauth/rails.rb +32 -13
  60. data/lib/rodauth/rails/auth.rb +9 -12
  61. data/lib/rodauth/rails/feature.rb +19 -230
  62. data/lib/rodauth/rails/feature/base.rb +62 -0
  63. data/lib/rodauth/rails/feature/callbacks.rb +65 -0
  64. data/lib/rodauth/rails/feature/csrf.rb +65 -0
  65. data/lib/rodauth/rails/feature/email.rb +30 -0
  66. data/lib/rodauth/rails/feature/instrumentation.rb +71 -0
  67. data/lib/rodauth/rails/feature/internal_request.rb +50 -0
  68. data/lib/rodauth/rails/feature/render.rb +48 -0
  69. data/lib/rodauth/rails/model.rb +101 -0
  70. data/lib/rodauth/rails/model/associations.rb +195 -0
  71. data/lib/rodauth/rails/railtie.rb +0 -5
  72. data/lib/rodauth/rails/tasks.rake +5 -5
  73. data/lib/rodauth/rails/version.rb +1 -1
  74. data/rodauth-rails.gemspec +4 -1
  75. metadata +56 -6
  76. data/lib/rodauth/rails/log_subscriber.rb +0 -34
@@ -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
+ def rails_controller
26
+ if only_json? && Rodauth::Rails.api_only?
27
+ ActionController::API
28
+ else
29
+ ActionController::Base
30
+ end
31
+ end
32
+
33
+ delegate :rails_routes, :rails_request, to: :scope
34
+
35
+ private
36
+
37
+ # Instances of the configured controller with current request's env hash.
38
+ def _rails_controller_instance
39
+ controller = rails_controller.new
40
+ prepare_rails_controller(controller, rails_request)
41
+ controller
42
+ end
43
+
44
+ if ActionPack.version >= Gem::Version.new("5.0")
45
+ def prepare_rails_controller(controller, rails_request)
46
+ controller.set_request! rails_request
47
+ controller.set_response! rails_controller.make_response!(rails_request)
48
+ end
49
+ else
50
+ def prepare_rails_controller(controller, rails_request)
51
+ controller.send(:set_response!, rails_request)
52
+ controller.instance_variable_set(:@_request, rails_request)
53
+ end
54
+ end
55
+
56
+ def rails_api_controller?
57
+ defined?(ActionController::API) && rails_controller <= ActionController::API
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,65 @@
1
+ module Rodauth
2
+ module Rails
3
+ module Feature
4
+ module Callbacks
5
+ private
6
+
7
+ def _around_rodauth
8
+ rails_controller_around { super }
9
+ end
10
+
11
+ # Runs controller callbacks and rescue handlers around Rodauth actions.
12
+ def rails_controller_around
13
+ result = nil
14
+
15
+ rails_controller_rescue do
16
+ rails_controller_callbacks do
17
+ result = catch(:halt) { yield }
18
+ end
19
+ end
20
+
21
+ result = handle_rails_controller_response(result)
22
+
23
+ throw :halt, result if result
24
+ end
25
+
26
+ # Runs any #(before|around|after)_action controller callbacks.
27
+ def rails_controller_callbacks(&block)
28
+ rails_controller_instance.run_callbacks(:process_action, &block)
29
+ end
30
+
31
+ # Runs any registered #rescue_from controller handlers.
32
+ def rails_controller_rescue
33
+ yield
34
+ rescue Exception => exception
35
+ rails_controller_instance.rescue_with_handler(exception) || raise
36
+
37
+ unless rails_controller_instance.performed?
38
+ raise Rodauth::Rails::Error, "rescue_from handler didn't write any response"
39
+ end
40
+ end
41
+
42
+ # Handles controller rendering a response or setting response headers.
43
+ def handle_rails_controller_response(result)
44
+ if rails_controller_instance.performed?
45
+ rails_controller_response
46
+ elsif result
47
+ result[1].merge!(rails_controller_instance.response.headers)
48
+ result
49
+ end
50
+ end
51
+
52
+ # Returns Roda response from controller response if set.
53
+ def rails_controller_response
54
+ controller_response = rails_controller_instance.response
55
+
56
+ response.status = controller_response.status
57
+ response.headers.merge! controller_response.headers
58
+ response.write controller_response.body
59
+
60
+ response.finish
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,65 @@
1
+ module Rodauth
2
+ module Rails
3
+ module Feature
4
+ module Csrf
5
+ def self.included(feature)
6
+ feature.auth_methods(
7
+ :rails_csrf_tag,
8
+ :rails_csrf_param,
9
+ :rails_csrf_token,
10
+ :rails_check_csrf!,
11
+ )
12
+ end
13
+
14
+ # Render Rails CSRF tags in Rodauth templates.
15
+ def csrf_tag(*)
16
+ rails_csrf_tag
17
+ end
18
+
19
+ # Verify Rails' authenticity token.
20
+ def check_csrf
21
+ rails_check_csrf!
22
+ end
23
+
24
+ # Have Rodauth call #check_csrf automatically.
25
+ def check_csrf?
26
+ true
27
+ end
28
+
29
+ private
30
+
31
+ def rails_controller_callbacks
32
+ return super if rails_api_controller?
33
+
34
+ # don't verify CSRF token as part of callbacks, Rodauth will do that
35
+ rails_controller_instance.allow_forgery_protection = false
36
+ super do
37
+ # turn the setting back to default so that form tags generate CSRF tags
38
+ rails_controller_instance.allow_forgery_protection = rails_controller.allow_forgery_protection
39
+ yield
40
+ end
41
+ end
42
+
43
+ # Calls the controller to verify the authenticity token.
44
+ def rails_check_csrf!
45
+ rails_controller_instance.send(:verify_authenticity_token)
46
+ end
47
+
48
+ # Hidden tag with Rails CSRF token inserted into Rodauth templates.
49
+ def rails_csrf_tag
50
+ %(<input type="hidden" name="#{rails_csrf_param}" value="#{rails_csrf_token}">)
51
+ end
52
+
53
+ # The request parameter under which to send the Rails CSRF token.
54
+ def rails_csrf_param
55
+ rails_controller.request_forgery_protection_token
56
+ end
57
+
58
+ # The Rails CSRF token value inserted into Rodauth templates.
59
+ def rails_csrf_token
60
+ rails_controller_instance.send(:form_authenticity_token)
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,30 @@
1
+ module Rodauth
2
+ module Rails
3
+ module Feature
4
+ module Email
5
+ def self.included(feature)
6
+ feature.depends :email_base
7
+ end
8
+
9
+ private
10
+
11
+ # Create emails with ActionMailer which uses configured delivery method.
12
+ def create_email_to(to, subject, body)
13
+ Mailer.create_email(to: to, from: email_from, subject: "#{email_subject_prefix}#{subject}", body: body)
14
+ end
15
+
16
+ # Delivers the given email.
17
+ def send_email(email)
18
+ email.deliver_now
19
+ end
20
+
21
+ # ActionMailer subclass for correct email delivering.
22
+ class Mailer < ActionMailer::Base
23
+ def create_email(**options)
24
+ mail(**options)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,71 @@
1
+ module Rodauth
2
+ module Rails
3
+ module Feature
4
+ module Instrumentation
5
+ private
6
+
7
+ def _around_rodauth
8
+ rails_instrument_request { super }
9
+ end
10
+
11
+ def redirect(*)
12
+ rails_instrument_redirection { super }
13
+ end
14
+
15
+ def rails_render(*)
16
+ render_output = nil
17
+ rails_controller_instance.view_runtime = rails_controller_instance.send(:cleanup_view_runtime) do
18
+ Benchmark.ms { render_output = super }
19
+ end
20
+ render_output
21
+ end
22
+
23
+ def rails_instrument_request
24
+ request = rails_request
25
+
26
+ raw_payload = {
27
+ controller: scope.class.superclass.name,
28
+ action: "call",
29
+ request: request,
30
+ params: request.filtered_parameters,
31
+ headers: request.headers,
32
+ format: request.format.ref,
33
+ method: request.request_method,
34
+ path: request.fullpath
35
+ }
36
+
37
+ ActiveSupport::Notifications.instrument("start_processing.action_controller", raw_payload)
38
+
39
+ ActiveSupport::Notifications.instrument("process_action.action_controller", raw_payload) do |payload|
40
+ begin
41
+ result = catch(:halt) { yield }
42
+
43
+ response = ActionDispatch::Response.new *(result || [404, {}, []])
44
+ payload[:response] = response
45
+ payload[:status] = response.status
46
+
47
+ throw :halt, result if result
48
+ rescue => error
49
+ payload[:status] = ActionDispatch::ExceptionWrapper.status_code_for_exception(error.class.name)
50
+ raise
51
+ ensure
52
+ rails_controller_eval { append_info_to_payload(payload) }
53
+ end
54
+ end
55
+ end
56
+
57
+ def rails_instrument_redirection
58
+ ActiveSupport::Notifications.instrument("redirect_to.action_controller", request: rails_request) do |payload|
59
+ result = catch(:halt) { yield }
60
+
61
+ response = ActionDispatch::Response.new(*result)
62
+ payload[:status] = response.status
63
+ payload[:location] = response.filtered_location
64
+
65
+ throw :halt, result
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,50 @@
1
+ module Rodauth
2
+ module Rails
3
+ module Feature
4
+ module InternalRequest
5
+ def post_configure
6
+ super
7
+ return unless internal_request?
8
+
9
+ self.class.define_singleton_method(:internal_request) do |route, opts = {}, &blk|
10
+ url_options = ::Rails.application.config.action_mailer.default_url_options || {}
11
+
12
+ scheme = url_options[:protocol]
13
+ port = url_options[:port]
14
+ port||= Rack::Request::DEFAULT_PORTS[scheme] if Rack.release < "2"
15
+ host = url_options[:host]
16
+ host_with_port = host && port ? "#{host}:#{port}" : host
17
+
18
+ env = {
19
+ "HTTP_HOST" => host_with_port,
20
+ "rack.url_scheme" => scheme,
21
+ "SERVER_NAME" => host,
22
+ "SERVER_PORT" => port,
23
+ }.compact
24
+
25
+ opts = opts.merge(env: env) { |k, v1, v2| v2.merge(v1) }
26
+
27
+ super(route, opts, &blk)
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def rails_controller_around
34
+ return yield if internal_request?
35
+ super
36
+ end
37
+
38
+ def rails_instrument_request
39
+ return yield if internal_request?
40
+ super
41
+ end
42
+
43
+ def rails_instrument_redirection
44
+ return yield if internal_request?
45
+ super
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,48 @@
1
+ module Rodauth
2
+ module Rails
3
+ module Feature
4
+ module Render
5
+ def self.included(feature)
6
+ feature.auth_methods :rails_render
7
+ end
8
+
9
+ # Renders templates with layout. First tries to render a user-defined
10
+ # template, otherwise falls back to Rodauth's template.
11
+ def view(page, *)
12
+ rails_render(action: page.tr("-", "_"), layout: true) ||
13
+ rails_render(html: super.html_safe, layout: true)
14
+ end
15
+
16
+ # Renders templates without layout. First tries to render a user-defined
17
+ # template or partial, otherwise falls back to Rodauth's template.
18
+ def render(page)
19
+ rails_render(partial: page.tr("-", "_"), layout: false) ||
20
+ rails_render(action: page.tr("-", "_"), layout: false) ||
21
+ super.html_safe
22
+ end
23
+
24
+ def button(*)
25
+ super.html_safe
26
+ end
27
+
28
+ private
29
+
30
+ # Calls the Rails renderer, returning nil if a template is missing.
31
+ def rails_render(*args)
32
+ return if rails_api_controller?
33
+
34
+ rails_controller_instance.render_to_string(*args)
35
+ rescue ActionView::MissingTemplate
36
+ nil
37
+ end
38
+
39
+ # Only look up template formats that the current request is accepting.
40
+ def _rails_controller_instance
41
+ controller = super
42
+ controller.formats = rails_request.formats.map(&:ref).compact
43
+ controller
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,101 @@
1
+ module Rodauth
2
+ module Rails
3
+ class Model < Module
4
+ require "rodauth/rails/model/associations"
5
+
6
+ def initialize(auth_class, association_options: {})
7
+ @auth_class = auth_class
8
+ @association_options = association_options
9
+
10
+ define_methods
11
+ end
12
+
13
+ def included(model)
14
+ fail Rodauth::Rails::Error, "must be an Active Record model" unless model < ActiveRecord::Base
15
+
16
+ define_associations(model)
17
+ end
18
+
19
+ private
20
+
21
+ def define_methods
22
+ rodauth = @auth_class.allocate.freeze
23
+
24
+ attr_reader :password
25
+
26
+ define_method(:password=) do |password|
27
+ @password = password
28
+ password_hash = rodauth.send(:password_hash, password) if password
29
+ set_password_hash(password_hash)
30
+ end
31
+
32
+ define_method(:set_password_hash) do |password_hash|
33
+ if rodauth.account_password_hash_column
34
+ public_send(:"#{rodauth.account_password_hash_column}=", password_hash)
35
+ else
36
+ if password_hash
37
+ record = self.password_hash || build_password_hash
38
+ record.public_send(:"#{rodauth.password_hash_column}=", password_hash)
39
+ else
40
+ self.password_hash&.mark_for_destruction
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ def define_associations(model)
47
+ define_password_hash_association(model) unless rodauth.account_password_hash_column
48
+
49
+ feature_associations.each do |association|
50
+ define_association(model, **association)
51
+ end
52
+ end
53
+
54
+ def define_password_hash_association(model)
55
+ password_hash_id_column = rodauth.password_hash_id_column
56
+ scope = -> { select(password_hash_id_column) } if rodauth.send(:use_database_authentication_functions?)
57
+
58
+ define_association model,
59
+ type: :has_one,
60
+ name: :password_hash,
61
+ table: rodauth.password_hash_table,
62
+ foreign_key: password_hash_id_column,
63
+ scope: scope,
64
+ autosave: true
65
+ end
66
+
67
+ def define_association(model, type:, name:, table:, foreign_key:, scope: nil, **options)
68
+ associated_model = Class.new(model.superclass)
69
+ associated_model.table_name = table
70
+ associated_model.belongs_to :account,
71
+ class_name: model.name,
72
+ foreign_key: foreign_key,
73
+ inverse_of: name
74
+
75
+ model.const_set(name.to_s.singularize.camelize, associated_model)
76
+
77
+ model.public_send type, name, scope,
78
+ class_name: associated_model.name,
79
+ foreign_key: foreign_key,
80
+ dependent: :destroy,
81
+ inverse_of: :account,
82
+ **options,
83
+ **association_options(name)
84
+ end
85
+
86
+ def feature_associations
87
+ Rodauth::Rails::Model::Associations.call(rodauth)
88
+ end
89
+
90
+ def association_options(name)
91
+ options = @association_options
92
+ options = options.call(name) if options.respond_to?(:call)
93
+ options || {}
94
+ end
95
+
96
+ def rodauth
97
+ @auth_class.allocate
98
+ end
99
+ end
100
+ end
101
+ end