rodauth-rails 0.3.1 → 0.6.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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -0
  3. data/README.md +169 -69
  4. data/lib/generators/rodauth/install_generator.rb +34 -17
  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/{lib → app/lib}/rodauth_app.rb +26 -2
  26. data/lib/generators/rodauth/templates/config/initializers/sequel.rb +1 -5
  27. data/lib/generators/rodauth/templates/db/migrate/create_rodauth.rb +2 -167
  28. data/lib/rodauth/rails.rb +24 -5
  29. data/lib/rodauth/rails/app.rb +5 -4
  30. data/lib/rodauth/rails/feature.rb +69 -13
  31. data/lib/rodauth/rails/flash.rb +48 -0
  32. data/lib/rodauth/rails/railtie.rb +11 -0
  33. data/lib/rodauth/rails/tasks.rake +28 -0
  34. data/lib/rodauth/rails/version.rb +5 -0
  35. data/rodauth-rails.gemspec +6 -4
  36. metadata +31 -9
  37. data/lib/rodauth/rails/app/flash.rb +0 -50
@@ -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,170 +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
- # Used by the remember me feature
48
- create_table :account_remember_keys do |t|
49
- t.foreign_key :accounts, column: :id
50
- t.string :key, null: false
51
- t.datetime :deadline, null: false
52
- end
53
-
54
- # # Used by the audit logging feature
55
- # create_table :account_authentication_audit_logs do |t|
56
- # t.references :account, foreign_key: true, null: false
57
- # t.datetime :at, null: false, default: -> { "CURRENT_TIMESTAMP" }
58
- # t.text :message, null: false
59
- <% case activerecord_adapter -%>
60
- <% when "postgresql" -%>
61
- # t.jsonb :metadata
62
- <% when "sqlite3", "mysql2" -%>
63
- # t.json :metadata
64
- <% else -%>
65
- # t.string :metadata
66
- <% end -%>
67
- # t.index [:account_id, :at], name: "audit_account_at_idx"
68
- # t.index :at, name: "audit_at_idx"
69
- # end
70
-
71
- # # Used by the jwt refresh feature
72
- # create_table :account_jwt_refresh_keys do |t|
73
- # t.references :account, foreign_key: true, null: false
74
- # t.string :key, null: false
75
- # t.datetime :deadline, null: false
76
- # t.index :account_id, name: "account_jwt_rk_account_id_idx"
77
- # end
78
-
79
- # # Used by the disallow_password_reuse feature
80
- # create_table :account_previous_password_hashes do |t|
81
- # t.references :account, foreign_key: true
82
- # t.string :password_hash, null: false
83
- # end
84
-
85
- # # Used by the lockout feature
86
- # create_table :account_login_failures do |t|
87
- # t.foreign_key :accounts, column: :id
88
- # t.integer :number, null: false, default: 1
89
- # end
90
- # create_table :account_lockouts do |t|
91
- # t.foreign_key :accounts, column: :id
92
- # t.string :key, null: false
93
- # t.datetime :deadline, null: false
94
- # t.datetime :email_last_sent
95
- # end
96
-
97
- # # Used by the email auth feature
98
- # create_table :account_email_auth_keys do |t|
99
- # t.foreign_key :accounts, column: :id
100
- # t.string :key, null: false
101
- # t.datetime :deadline, null: false
102
- # t.datetime :email_last_sent, null: false, default: -> { "CURRENT_TIMESTAMP" }
103
- # end
104
-
105
- # # Used by the password expiration feature
106
- # create_table :account_password_change_times do |t|
107
- # t.foreign_key :accounts, column: :id
108
- # t.datetime :changed_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
109
- # end
110
-
111
- # # Used by the account expiration feature
112
- # create_table :account_activity_times do |t|
113
- # t.foreign_key :accounts, column: :id
114
- # t.datetime :last_activity_at, null: false
115
- # t.datetime :last_login_at, null: false
116
- # t.datetime :expired_at
117
- # end
118
-
119
- # # Used by the single session feature
120
- # create_table :account_session_keys do |t|
121
- # t.foreign_key :accounts, column: :id
122
- # t.string :key, null: false
123
- # end
124
-
125
- # # Used by the active sessions feature
126
- # create_table :account_active_session_keys, primary_key: [:account_id, :session_id] do |t|
127
- # t.references :account, foreign_key: true
128
- # t.string :session_id
129
- # t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
130
- # t.datetime :last_use, null: false, default: -> { "CURRENT_TIMESTAMP" }
131
- # end
132
-
133
- # # Used by the webauthn feature
134
- # create_table :account_webauthn_user_ids do |t|
135
- # t.foreign_key :accounts, column: :id
136
- # t.string :webauthn_id, null: false
137
- # end
138
- # create_table :account_webauthn_keys, primary_key: [:account_id, :webauthn_id] do |t|
139
- # t.references :account, foreign_key: true
140
- # t.string :webauthn_id
141
- # t.string :public_key, null: false
142
- # t.integer :sign_count, null: false
143
- # t.datetime :last_use, null: false, default: -> { "CURRENT_TIMESTAMP" }
144
- # end
145
-
146
- # # Used by the otp feature
147
- # create_table :account_otp_keys do |t|
148
- # t.foreign_key :accounts, column: :id
149
- # t.string :key, null: false
150
- # t.integer :num_failures, null: false, default: 0
151
- # t.datetime :last_use, null: false, default: -> { "CURRENT_TIMESTAMP" }
152
- # end
153
-
154
- # # Used by the recovery codes feature
155
- # create_table :account_recovery_codes, primary_key: [:id, :code] do |t|
156
- # t.integer :id
157
- # t.foreign_key :accounts, column: :id
158
- # t.string :code
159
- # end
160
-
161
- # # Used by the sms codes feature
162
- # create_table :account_sms_codes do |t|
163
- # t.foreign_key :accounts, column: :id
164
- # t.string :phone_number, null: false
165
- # t.integer :num_failures
166
- # t.string :code
167
- # t.datetime :code_issued_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
168
- # end
3
+ <%= migration_content -%>
169
4
  end
170
5
  end
@@ -1,4 +1,4 @@
1
- require "rodauth/version"
1
+ require "rodauth/rails/version"
2
2
  require "rodauth/rails/railtie"
3
3
 
4
4
  module Rodauth
@@ -9,14 +9,33 @@ 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
+ def configure
36
+ yield self
37
+ end
38
+
20
39
  attr_writer :app
21
40
  attr_writer :middleware
22
41
 
@@ -4,15 +4,16 @@ module Rodauth
4
4
  module Rails
5
5
  # The superclass for creating a Rodauth middleware.
6
6
  class App < Roda
7
- require "rodauth/rails/app/flash"
8
-
9
7
  plugin :middleware
10
8
  plugin :hooks
11
9
  plugin :render, layout: false
12
10
 
13
- plugin Flash
14
-
15
11
  def self.configure(name = nil, **options, &block)
12
+ unless options[:json] == :only
13
+ require "rodauth/rails/flash"
14
+ plugin Flash
15
+ end
16
+
16
17
  plugin :rodauth, name: name, csrf: false, flash: false, **options do
17
18
  # load the Rails integration
18
19
  enable :rails
@@ -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,11 +44,6 @@ 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
@@ -50,6 +51,59 @@ module Rodauth
50
51
 
51
52
  private
52
53
 
54
+ # Runs controller callbacks and rescue handlers around Rodauth actions.
55
+ def _around_rodauth(&block)
56
+ result = nil
57
+
58
+ rails_controller_rescue do
59
+ rails_controller_callbacks do
60
+ result = catch(:halt) { super(&block) }
61
+ end
62
+ end
63
+
64
+ if rails_controller_instance.performed?
65
+ rails_controller_response
66
+ else
67
+ result[1].merge!(rails_controller_instance.response.headers)
68
+ throw :halt, result
69
+ end
70
+ end
71
+
72
+ # Runs any #(before|around|after)_action controller callbacks.
73
+ def rails_controller_callbacks
74
+ # don't verify CSRF token as part of callbacks, Rodauth will do that
75
+ rails_controller_instance.allow_forgery_protection = false
76
+
77
+ rails_controller_instance.run_callbacks(:process_action) do
78
+ # turn the setting back to default so that form tags generate CSRF tags
79
+ rails_controller_instance.allow_forgery_protection = rails_controller.allow_forgery_protection
80
+
81
+ yield
82
+ end
83
+ end
84
+
85
+ # Runs any registered #rescue_from controller handlers.
86
+ def rails_controller_rescue
87
+ yield
88
+ rescue Exception => exception
89
+ rails_controller_instance.rescue_with_handler(exception) || raise
90
+
91
+ unless rails_controller_instance.performed?
92
+ raise Rodauth::Rails::Error, "rescue_from handler didn't write any response"
93
+ end
94
+ end
95
+
96
+ # Returns Roda response from controller response if set.
97
+ def rails_controller_response
98
+ controller_response = rails_controller_instance.response
99
+
100
+ response.status = controller_response.status
101
+ response.headers.merge! controller_response.headers
102
+ response.write controller_response.body
103
+
104
+ request.halt
105
+ end
106
+
53
107
  # Create emails with ActionMailer which uses configured delivery method.
54
108
  def create_email_to(to, subject, body)
55
109
  Mailer.create_email(to: to, from: email_from, subject: "#{email_subject_prefix}#{subject}", body: body)
@@ -62,11 +116,18 @@ module Rodauth
62
116
 
63
117
  # Calls the Rails renderer, returning nil if a template is missing.
64
118
  def rails_render(*args)
119
+ return if only_json?
120
+
65
121
  rails_controller_instance.render_to_string(*args)
66
122
  rescue ActionView::MissingTemplate
67
123
  nil
68
124
  end
69
125
 
126
+ # Calls the controller to verify the authenticity token.
127
+ def rails_check_csrf!
128
+ rails_controller_instance.send(:verify_authenticity_token)
129
+ end
130
+
70
131
  # Hidden tag with Rails CSRF token inserted into Rodauth templates.
71
132
  def rails_csrf_tag
72
133
  %(<input type="hidden" name="#{rails_csrf_param}" value="#{rails_csrf_token}">)
@@ -82,17 +143,12 @@ module Rodauth
82
143
  rails_controller_instance.send(:form_authenticity_token)
83
144
  end
84
145
 
85
- # Calls the controller to verify the authenticity token.
86
- def rails_check_csrf!
87
- rails_controller_instance.send(:verify_authenticity_token)
88
- end
89
-
90
146
  # Instances of the configured controller with current request's env hash.
91
- def rails_controller_instance
147
+ def _rails_controller_instance
92
148
  request = ActionDispatch::Request.new(scope.env)
93
149
  instance = rails_controller.new
94
150
 
95
- if ActionPack.version >= Gem::Version.new("5.0.0")
151
+ if ActionPack.version >= Gem::Version.new("5.0")
96
152
  instance.set_request! request
97
153
  instance.set_response! rails_controller.make_response!(request)
98
154
  else
@@ -0,0 +1,48 @@
1
+ module Rodauth
2
+ module Rails
3
+ # Roda plugin that sets up Rails flash integration.
4
+ module Flash
5
+ def self.load_dependencies(app)
6
+ app.plugin :hooks
7
+ end
8
+
9
+ def self.configure(app)
10
+ app.before { request.flash } # load flash
11
+ app.after { request.commit_flash } # save flash
12
+ end
13
+
14
+ module InstanceMethods
15
+ def flash
16
+ request.flash
17
+ end
18
+ end
19
+
20
+ module RequestMethods
21
+ # If the redirect would bubble up outside of the Roda app, the after
22
+ # hook would never get called, so we make sure to commit the flash.
23
+ def redirect(*)
24
+ commit_flash
25
+ super
26
+ end
27
+
28
+ def flash
29
+ rails_request.flash
30
+ end
31
+
32
+ def commit_flash
33
+ if ActionPack.version >= Gem::Version.new("5.0")
34
+ rails_request.commit_flash
35
+ else
36
+ # ActionPack 4.2 automatically commits flash
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def rails_request
43
+ ActionDispatch::Request.new(env)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -1,6 +1,8 @@
1
1
  require "rodauth/rails/middleware"
2
2
  require "rodauth/rails/controller_methods"
3
3
 
4
+ require "rails"
5
+
4
6
  module Rodauth
5
7
  module Rails
6
8
  class Railtie < ::Rails::Railtie
@@ -13,6 +15,15 @@ module Rodauth
13
15
  include Rodauth::Rails::ControllerMethods
14
16
  end
15
17
  end
18
+
19
+ initializer "rodauth.test" do
20
+ # Rodauth uses RACK_ENV to set the default bcrypt hash cost
21
+ ENV["RACK_ENV"] = "test" if ::Rails.env.test?
22
+ end
23
+
24
+ rake_tasks do
25
+ load "rodauth/rails/tasks.rake"
26
+ end
16
27
  end
17
28
  end
18
29
  end