rodauth-rails 0.3.1 → 0.6.0

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