rodauth-rails 0.11.0 → 0.15.0

Sign up to get free protection for your applications and to get access to all the features.
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