rodauth-rails 0.4.2 → 0.8.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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +39 -1
  3. data/README.md +419 -83
  4. data/lib/generators/rodauth/install_generator.rb +38 -23
  5. data/lib/generators/rodauth/migration/account_expiration.erb +7 -0
  6. data/lib/generators/rodauth/migration/active_sessions.erb +7 -0
  7. data/lib/generators/rodauth/migration/audit_logging.erb +16 -0
  8. data/lib/generators/rodauth/migration/base.erb +19 -0
  9. data/lib/generators/rodauth/migration/disallow_password_reuse.erb +5 -0
  10. data/lib/generators/rodauth/migration/email_auth.erb +7 -0
  11. data/lib/generators/rodauth/migration/jwt_refresh.erb +7 -0
  12. data/lib/generators/rodauth/migration/lockout.erb +11 -0
  13. data/lib/generators/rodauth/migration/otp.erb +7 -0
  14. data/lib/generators/rodauth/migration/password_expiration.erb +5 -0
  15. data/lib/generators/rodauth/migration/recovery_codes.erb +6 -0
  16. data/lib/generators/rodauth/migration/remember.erb +6 -0
  17. data/lib/generators/rodauth/migration/reset_password.erb +7 -0
  18. data/lib/generators/rodauth/migration/single_session.erb +5 -0
  19. data/lib/generators/rodauth/migration/sms_codes.erb +8 -0
  20. data/lib/generators/rodauth/migration/verify_account.erb +7 -0
  21. data/lib/generators/rodauth/migration/verify_login_change.erb +7 -0
  22. data/lib/generators/rodauth/migration/webauthn.erb +12 -0
  23. data/lib/generators/rodauth/migration_generator.rb +32 -0
  24. data/lib/generators/rodauth/migration_helpers.rb +69 -0
  25. data/lib/generators/rodauth/templates/app/controllers/rodauth_controller.rb +2 -1
  26. data/lib/generators/rodauth/templates/app/lib/rodauth_app.rb +18 -20
  27. data/lib/generators/rodauth/templates/config/initializers/sequel.rb +1 -5
  28. data/lib/generators/rodauth/templates/db/migrate/create_rodauth.rb +2 -176
  29. data/lib/rodauth/rails.rb +33 -4
  30. data/lib/rodauth/rails/app.rb +6 -2
  31. data/lib/rodauth/rails/app/flash.rb +1 -1
  32. data/lib/rodauth/rails/app/middleware.rb +26 -0
  33. data/lib/rodauth/rails/feature.rb +100 -30
  34. data/lib/rodauth/rails/railtie.rb +6 -0
  35. data/lib/rodauth/rails/tasks.rake +28 -0
  36. data/lib/rodauth/rails/version.rb +1 -1
  37. data/rodauth-rails.gemspec +1 -1
  38. metadata +26 -5
  39. data/lib/rodauth/features/rails.rb +0 -1
@@ -1,8 +1,4 @@
1
1
  require "sequel/core"
2
2
 
3
3
  # initialize Sequel and have it reuse Active Record's database connection
4
- <% if RUBY_ENGINE == "jruby" -%>
5
- DB = Sequel.connect("jdbc:<%= sequel_adapter %>://", extensions: :activerecord_connection)
6
- <% else -%>
7
- DB = Sequel.<%= sequel_adapter %>(extensions: :activerecord_connection)
8
- <% end -%>
4
+ DB = Sequel.connect("<%= sequel_uri_scheme %>://", extensions: :activerecord_connection)
@@ -1,179 +1,5 @@
1
- class CreateRodauth < ActiveRecord::Migration<%= migration_version %>
1
+ class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version %>
2
2
  def change
3
- <% if activerecord_adapter == "postgresql" -%>
4
- enable_extension "citext"
5
-
6
- <% end -%>
7
- create_table :accounts do |t|
8
- <% case activerecord_adapter -%>
9
- <% when "postgresql" -%>
10
- t.citext :email, null: false, index: { unique: true, where: "status IN ('verified', 'unverified')" }
11
- <% else -%>
12
- t.string :email, null: false, index: { unique: true }
13
- <% end -%>
14
- t.string :status, null: false, default: "verified"
15
- end
16
-
17
- # Used if storing password hashes in a separate table (default)
18
- create_table :account_password_hashes do |t|
19
- t.foreign_key :accounts, column: :id
20
- t.string :password_hash, null: false
21
- end
22
-
23
- # Used by the password reset feature
24
- create_table :account_password_reset_keys do |t|
25
- t.foreign_key :accounts, column: :id
26
- t.string :key, null: false
27
- t.datetime :deadline, null: false
28
- t.datetime :email_last_sent, null: false, default: -> { "CURRENT_TIMESTAMP" }
29
- end
30
-
31
- # Used by the account verification feature
32
- create_table :account_verification_keys do |t|
33
- t.foreign_key :accounts, column: :id
34
- t.string :key, null: false
35
- t.datetime :requested_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
36
- t.datetime :email_last_sent, null: false, default: -> { "CURRENT_TIMESTAMP" }
37
- end
38
-
39
- # Used by the verify login change feature
40
- create_table :account_login_change_keys do |t|
41
- t.foreign_key :accounts, column: :id
42
- t.string :key, null: false
43
- t.string :login, null: false
44
- t.datetime :deadline, null: false
45
- end
46
-
47
- <% unless api_only? -%>
48
- # Used by the remember me feature
49
- create_table :account_remember_keys do |t|
50
- t.foreign_key :accounts, column: :id
51
- t.string :key, null: false
52
- t.datetime :deadline, null: false
53
- end
54
- <% else -%>
55
- # # Used by the remember me feature
56
- # create_table :account_remember_keys do |t|
57
- # t.foreign_key :accounts, column: :id
58
- # t.string :key, null: false
59
- # t.datetime :deadline, null: false
60
- # end
61
- <% end -%>
62
-
63
- # # Used by the audit logging feature
64
- # create_table :account_authentication_audit_logs do |t|
65
- # t.references :account, foreign_key: true, null: false
66
- # t.datetime :at, null: false, default: -> { "CURRENT_TIMESTAMP" }
67
- # t.text :message, null: false
68
- <% case activerecord_adapter -%>
69
- <% when "postgresql" -%>
70
- # t.jsonb :metadata
71
- <% when "sqlite3", "mysql2" -%>
72
- # t.json :metadata
73
- <% else -%>
74
- # t.string :metadata
75
- <% end -%>
76
- # t.index [:account_id, :at], name: "audit_account_at_idx"
77
- # t.index :at, name: "audit_at_idx"
78
- # end
79
-
80
- # # Used by the jwt refresh feature
81
- # create_table :account_jwt_refresh_keys do |t|
82
- # t.references :account, foreign_key: true, null: false
83
- # t.string :key, null: false
84
- # t.datetime :deadline, null: false
85
- # t.index :account_id, name: "account_jwt_rk_account_id_idx"
86
- # end
87
-
88
- # # Used by the disallow_password_reuse feature
89
- # create_table :account_previous_password_hashes do |t|
90
- # t.references :account, foreign_key: true
91
- # t.string :password_hash, null: false
92
- # end
93
-
94
- # # Used by the lockout feature
95
- # create_table :account_login_failures do |t|
96
- # t.foreign_key :accounts, column: :id
97
- # t.integer :number, null: false, default: 1
98
- # end
99
- # create_table :account_lockouts do |t|
100
- # t.foreign_key :accounts, column: :id
101
- # t.string :key, null: false
102
- # t.datetime :deadline, null: false
103
- # t.datetime :email_last_sent
104
- # end
105
-
106
- # # Used by the email auth feature
107
- # create_table :account_email_auth_keys do |t|
108
- # t.foreign_key :accounts, column: :id
109
- # t.string :key, null: false
110
- # t.datetime :deadline, null: false
111
- # t.datetime :email_last_sent, null: false, default: -> { "CURRENT_TIMESTAMP" }
112
- # end
113
-
114
- # # Used by the password expiration feature
115
- # create_table :account_password_change_times do |t|
116
- # t.foreign_key :accounts, column: :id
117
- # t.datetime :changed_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
118
- # end
119
-
120
- # # Used by the account expiration feature
121
- # create_table :account_activity_times do |t|
122
- # t.foreign_key :accounts, column: :id
123
- # t.datetime :last_activity_at, null: false
124
- # t.datetime :last_login_at, null: false
125
- # t.datetime :expired_at
126
- # end
127
-
128
- # # Used by the single session feature
129
- # create_table :account_session_keys do |t|
130
- # t.foreign_key :accounts, column: :id
131
- # t.string :key, null: false
132
- # end
133
-
134
- # # Used by the active sessions feature
135
- # create_table :account_active_session_keys, primary_key: [:account_id, :session_id] do |t|
136
- # t.references :account, foreign_key: true
137
- # t.string :session_id
138
- # t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
139
- # t.datetime :last_use, null: false, default: -> { "CURRENT_TIMESTAMP" }
140
- # end
141
-
142
- # # Used by the webauthn feature
143
- # create_table :account_webauthn_user_ids do |t|
144
- # t.foreign_key :accounts, column: :id
145
- # t.string :webauthn_id, null: false
146
- # end
147
- # create_table :account_webauthn_keys, primary_key: [:account_id, :webauthn_id] do |t|
148
- # t.references :account, foreign_key: true
149
- # t.string :webauthn_id
150
- # t.string :public_key, null: false
151
- # t.integer :sign_count, null: false
152
- # t.datetime :last_use, null: false, default: -> { "CURRENT_TIMESTAMP" }
153
- # end
154
-
155
- # # Used by the otp feature
156
- # create_table :account_otp_keys do |t|
157
- # t.foreign_key :accounts, column: :id
158
- # t.string :key, null: false
159
- # t.integer :num_failures, null: false, default: 0
160
- # t.datetime :last_use, null: false, default: -> { "CURRENT_TIMESTAMP" }
161
- # end
162
-
163
- # # Used by the recovery codes feature
164
- # create_table :account_recovery_codes, primary_key: [:id, :code] do |t|
165
- # t.integer :id
166
- # t.foreign_key :accounts, column: :id
167
- # t.string :code
168
- # end
169
-
170
- # # Used by the sms codes feature
171
- # create_table :account_sms_codes do |t|
172
- # t.foreign_key :accounts, column: :id
173
- # t.string :phone_number, null: false
174
- # t.integer :num_failures
175
- # t.string :code
176
- # t.datetime :code_issued_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
177
- # end
3
+ <%= migration_content -%>
178
4
  end
179
5
  end
@@ -9,14 +9,43 @@ module Rodauth
9
9
  # This allows the developer to avoid loading Rodauth at boot time.
10
10
  autoload :App, "rodauth/rails/app"
11
11
 
12
- def self.configure
13
- yield self
14
- end
15
-
16
12
  @app = nil
17
13
  @middleware = true
18
14
 
19
15
  class << self
16
+ def rodauth(name = nil)
17
+ url_options = ActionMailer::Base.default_url_options
18
+
19
+ scheme = url_options[:protocol] || "http"
20
+ port = url_options[:port]
21
+ port ||= Rack::Request::DEFAULT_PORTS[scheme] if Gem::Version.new(Rack.release) < Gem::Version.new("2.0")
22
+ host = url_options[:host]
23
+ host += ":#{port}" if port
24
+
25
+ rack_env = {
26
+ "HTTP_HOST" => host,
27
+ "rack.url_scheme" => scheme,
28
+ }
29
+
30
+ scope = app.new(rack_env)
31
+
32
+ scope.rodauth(name)
33
+ end
34
+
35
+ if ::Rails.gem_version >= Gem::Version.new("5.2")
36
+ def secret_key_base
37
+ ::Rails.application.secret_key_base
38
+ end
39
+ else
40
+ def secret_key_base
41
+ ::Rails.application.secrets.secret_key_base
42
+ end
43
+ end
44
+
45
+ def configure
46
+ yield self
47
+ end
48
+
20
49
  attr_writer :app
21
50
  attr_writer :middleware
22
51
 
@@ -1,10 +1,14 @@
1
1
  require "roda"
2
+ require "rodauth"
3
+ require "rodauth/rails/feature"
2
4
 
3
5
  module Rodauth
4
6
  module Rails
5
7
  # The superclass for creating a Rodauth middleware.
6
8
  class App < Roda
7
- plugin :middleware
9
+ require "rodauth/rails/app/middleware"
10
+ plugin Middleware
11
+
8
12
  plugin :hooks
9
13
  plugin :render, layout: false
10
14
 
@@ -25,7 +29,7 @@ module Rodauth
25
29
  set_deadline_values? true
26
30
 
27
31
  # use HMACs for additional security
28
- hmac_secret { ::Rails.application.secrets.secret_key_base }
32
+ hmac_secret { Rodauth::Rails.secret_key_base }
29
33
 
30
34
  # evaluate user configuration
31
35
  instance_exec(&block)
@@ -1,7 +1,7 @@
1
1
  module Rodauth
2
2
  module Rails
3
3
  class App
4
- # Sets up Rails' flash integration.
4
+ # Roda plugin that sets up Rails flash integration.
5
5
  module Flash
6
6
  def self.load_dependencies(app)
7
7
  app.plugin :hooks
@@ -0,0 +1,26 @@
1
+ module Rodauth
2
+ module Rails
3
+ class App
4
+ # Roda plugin that extends middleware plugin by propagating response headers.
5
+ module Middleware
6
+ def self.load_dependencies(app)
7
+ app.plugin :hooks
8
+ end
9
+
10
+ def self.configure(app)
11
+ app.after do
12
+ if response.empty? && response.headers.any?
13
+ env["rodauth.rails.headers"] = response.headers
14
+ end
15
+ end
16
+
17
+ app.plugin :middleware, handle_result: -> (env, res) do
18
+ if headers = env.delete("rodauth.rails.headers")
19
+ res[1] = headers.merge(res[1])
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -9,10 +9,11 @@ module Rodauth
9
9
  :rails_csrf_param,
10
10
  :rails_csrf_token,
11
11
  :rails_check_csrf!,
12
- :rails_controller_instance,
13
12
  :rails_controller,
14
13
  )
15
14
 
15
+ auth_cached_method :rails_controller_instance
16
+
16
17
  # Renders templates with layout. First tries to render a user-defined
17
18
  # template, otherwise falls back to Rodauth's template.
18
19
  def view(page, *)
@@ -28,6 +29,11 @@ module Rodauth
28
29
  super
29
30
  end
30
31
 
32
+ # Render Rails CSRF tags in Rodauth templates.
33
+ def csrf_tag(*)
34
+ rails_csrf_tag
35
+ end
36
+
31
37
  # Verify Rails' authenticity token.
32
38
  def check_csrf
33
39
  rails_check_csrf!
@@ -38,18 +44,73 @@ module Rodauth
38
44
  true
39
45
  end
40
46
 
41
- # Render Rails CSRF tags in Rodauth templates.
42
- def csrf_tag(*)
43
- rails_csrf_tag
44
- end
45
-
46
47
  # Default the flash error key to Rails' default :alert.
47
48
  def flash_error_key
48
49
  :alert
49
50
  end
50
51
 
52
+ # Evaluates the block in context of a Rodauth controller instance.
53
+ def rails_controller_eval(&block)
54
+ rails_controller_instance.instance_exec(&block)
55
+ end
56
+
51
57
  private
52
58
 
59
+ # Runs controller callbacks and rescue handlers around Rodauth actions.
60
+ def _around_rodauth(&block)
61
+ result = nil
62
+
63
+ rails_controller_rescue do
64
+ rails_controller_callbacks do
65
+ result = catch(:halt) { super(&block) }
66
+ end
67
+ end
68
+
69
+ if rails_controller_instance.performed?
70
+ rails_controller_response
71
+ elsif result
72
+ result[1].merge!(rails_controller_instance.response.headers)
73
+ throw :halt, result
74
+ else
75
+ result
76
+ end
77
+ end
78
+
79
+ # Runs any #(before|around|after)_action controller callbacks.
80
+ def rails_controller_callbacks
81
+ # don't verify CSRF token as part of callbacks, Rodauth will do that
82
+ rails_controller_instance.allow_forgery_protection = false
83
+
84
+ rails_controller_instance.run_callbacks(:process_action) do
85
+ # turn the setting back to default so that form tags generate CSRF tags
86
+ rails_controller_instance.allow_forgery_protection = rails_controller.allow_forgery_protection
87
+
88
+ yield
89
+ end
90
+ end
91
+
92
+ # Runs any registered #rescue_from controller handlers.
93
+ def rails_controller_rescue
94
+ yield
95
+ rescue Exception => exception
96
+ rails_controller_instance.rescue_with_handler(exception) || raise
97
+
98
+ unless rails_controller_instance.performed?
99
+ raise Rodauth::Rails::Error, "rescue_from handler didn't write any response"
100
+ end
101
+ end
102
+
103
+ # Returns Roda response from controller response if set.
104
+ def rails_controller_response
105
+ controller_response = rails_controller_instance.response
106
+
107
+ response.status = controller_response.status
108
+ response.headers.merge! controller_response.headers
109
+ response.write controller_response.body
110
+
111
+ request.halt
112
+ end
113
+
53
114
  # Create emails with ActionMailer which uses configured delivery method.
54
115
  def create_email_to(to, subject, body)
55
116
  Mailer.create_email(to: to, from: email_from, subject: "#{email_subject_prefix}#{subject}", body: body)
@@ -64,11 +125,14 @@ module Rodauth
64
125
  def rails_render(*args)
65
126
  return if only_json?
66
127
 
67
- begin
68
- rails_controller_instance.render_to_string(*args)
69
- rescue ActionView::MissingTemplate
70
- nil
71
- end
128
+ rails_controller_instance.render_to_string(*args)
129
+ rescue ActionView::MissingTemplate
130
+ nil
131
+ end
132
+
133
+ # Calls the controller to verify the authenticity token.
134
+ def rails_check_csrf!
135
+ rails_controller_instance.send(:verify_authenticity_token)
72
136
  end
73
137
 
74
138
  # Hidden tag with Rails CSRF token inserted into Rodauth templates.
@@ -86,30 +150,36 @@ module Rodauth
86
150
  rails_controller_instance.send(:form_authenticity_token)
87
151
  end
88
152
 
89
- # Calls the controller to verify the authenticity token.
90
- def rails_check_csrf!
91
- rails_controller_instance.send(:verify_authenticity_token)
92
- end
93
-
94
153
  # Instances of the configured controller with current request's env hash.
95
- def rails_controller_instance
96
- request = ActionDispatch::Request.new(scope.env)
97
- instance = rails_controller.new
154
+ def _rails_controller_instance
155
+ controller = rails_controller.new
156
+ rails_request = ActionDispatch::Request.new(scope.env)
98
157
 
99
- if ActionPack.version >= Gem::Version.new("5.0")
100
- instance.set_request! request
101
- instance.set_response! rails_controller.make_response!(request)
102
- else
103
- instance.send(:set_response!, request)
104
- instance.instance_variable_set(:@_request, request)
105
- end
158
+ prepare_rails_controller(controller, rails_request)
106
159
 
107
- instance
160
+ controller
108
161
  end
109
162
 
110
- # Controller class to use for rendering and CSRF protection.
111
- def rails_controller
112
- ActionController::Base
163
+ if ActionPack.version >= Gem::Version.new("5.0")
164
+ # Controller class to use for view rendering, CSRF protection, and
165
+ # running any registered action callbacks and rescue_from handlers.
166
+ def rails_controller
167
+ only_json? ? ActionController::API : ActionController::Base
168
+ end
169
+
170
+ def prepare_rails_controller(controller, rails_request)
171
+ controller.set_request! rails_request
172
+ controller.set_response! rails_controller.make_response!(rails_request)
173
+ end
174
+ else
175
+ def rails_controller
176
+ ActionController::Base
177
+ end
178
+
179
+ def prepare_rails_controller(controller, rails_request)
180
+ controller.send(:set_response!, rails_request)
181
+ controller.instance_variable_set(:@_request, rails_request)
182
+ end
113
183
  end
114
184
 
115
185
  # ActionMailer subclass for correct email delivering.