rodauth-rails 1.2.1 → 1.3.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 43bfba245fc25ff73659728066a02b0227b0041823547767a10fb7025f354329
4
- data.tar.gz: 8e66a69d20e07b882e633330d27f7fafde524ba8e7af0052c83025bf101c3c10
3
+ metadata.gz: 4e268594e5890725cbba25ee24ab158df204ada3789aeebe428b737307128d9e
4
+ data.tar.gz: dca778863032dc428ac44b5feca48fe430ef55ea64f4e10050293d1c1a3d95c6
5
5
  SHA512:
6
- metadata.gz: e94b952207b08ba887d4168e442c669d15d04af11a0dada8f56d38f572ca1c662b0160067c2680cc984a203afe7cb443c1e3d9893544e9920655f4159c463d7f
7
- data.tar.gz: 56562e10ef5f361511fe090265b6647056f9befa8ba3d2095c90a5e07f18e4e61ba202a6dad28289ccaded5ba096a535ff38a98096e034d811f0749bcc1a1207
6
+ metadata.gz: 9008b2381959811820eca625f68c5df7ec2021319ceb2c7bdf6c75412f32ea51f01ffabd716da73f6565e263e0a74699c2c940a70296adf0b9c0b8576ef8e3de
7
+ data.tar.gz: 4b0d70d43ff624b610bcded93a528b089d103f11975b7fdec9c8bd38b1f81b5c003de2fa1e4068f2c2006841f91783044280fa575ec5951b2ae319f4836a49b7
data/CHANGELOG.md CHANGED
@@ -1,3 +1,21 @@
1
+ ## 1.3.1 (2022-04-22)
2
+
3
+ * Ensure response status is logged when calling a halting rodauth method inside a controller (@janko)
4
+
5
+ ## 1.3.0 (2022-04-01)
6
+
7
+ * Store password hash on the `accounts` table in generated Rodauth migration and configuration (@janko)
8
+
9
+ * Add support for controller testing with Minitest or RSpec (@janko)
10
+
11
+ * Fix `enum` declaration in generated `Account` model for Active Record < 7.0 (@janko)
12
+
13
+ * Ensure `require_login_redirect` points to the login page even if the login route changes (@janko)
14
+
15
+ ## 1.2.2 (2022-02-22)
16
+
17
+ * Fix flash messages not being preserved through consecutive redirects (@janko)
18
+
1
19
  ## 1.2.1 (2022-02-19)
2
20
 
3
21
  * Change `accounts.status` column type from string to integer (@zhongsheng)
data/README.md CHANGED
@@ -10,7 +10,6 @@ Provides Rails integration for the [Rodauth] authentication framework.
10
10
  * [Rails demo](https://github.com/janko/rodauth-demo-rails)
11
11
  * [JSON API guide](https://github.com/janko/rodauth-rails/wiki/JSON-API)
12
12
  * [OmniAuth guide](https://github.com/janko/rodauth-rails/wiki/OmniAuth)
13
- * [Testing guide](https://github.com/janko/rodauth-rails/wiki/Testing)
14
13
 
15
14
  🎥 Screencasts:
16
15
 
@@ -40,7 +39,7 @@ of the advantages that stand out for me:
40
39
  * consistent before/after hooks around everything
41
40
  * dedicated object encapsulating all authentication logic
42
41
 
43
- One commmon concern is the fact that, unlike most other authentication
42
+ One common concern is the fact that, unlike most other authentication
44
43
  frameworks for Rails, Rodauth uses [Sequel] for database interaction instead of
45
44
  Active Record. There are good reasons for this, and to make Rodauth work
46
45
  smoothly alongside Active Record, rodauth-rails configures Sequel to [reuse
@@ -783,6 +782,86 @@ Rodauth::Rails.rodauth(session: { two_factor_auth_setup: true })
783
782
  Rodauth::Rails.rodauth(:admin, params: { "param" => "value" })
784
783
  ```
785
784
 
785
+ ## Testing
786
+
787
+ For system and integration tests, which run the whole middleware stack,
788
+ authentication can be exercised normally via HTTP endpoints. See [this wiki
789
+ page](https://github.com/janko/rodauth-rails/wiki/Testing) for some examples.
790
+
791
+ For controller tests, you can log in accounts by modifying the session:
792
+
793
+ ```rb
794
+ # app/controllers/articles_controller.rb
795
+ class ArticlesController < ApplicationController
796
+ before_action -> { rodauth.require_authentication }
797
+
798
+ def index
799
+ # ...
800
+ end
801
+ end
802
+ ```
803
+ ```rb
804
+ # test/controllers/articles_controller_test.rb
805
+ class ArticlesControllerTest < ActionController::TestCase
806
+ test "required authentication" do
807
+ get :index
808
+
809
+ assert_response 302
810
+ assert_redirected_to "/login"
811
+ assert_equal "Please login to continue", flash[:alert]
812
+
813
+ account = Account.create!(email: "user@example.com", password: "secret", status: "verified")
814
+ login(account)
815
+
816
+ get :index
817
+ assert_response 200
818
+
819
+ logout
820
+
821
+ get :index
822
+ assert_response 302
823
+ assert_equal "Please login to continue", flash[:alert]
824
+ end
825
+
826
+ private
827
+
828
+ # Manually modify the session into what Rodauth expects.
829
+ def login(account)
830
+ session[:account_id] = account.id
831
+ session[:authenticated_by] = ["password"] # or ["password", "totp"] for MFA
832
+ end
833
+
834
+ def logout
835
+ session.clear
836
+ end
837
+ end
838
+ ```
839
+
840
+ If you're using multiple configurations with different session prefixes, you'll need
841
+ to make sure to use those in controller tests as well:
842
+
843
+ ```rb
844
+ class RodauthAdmin < Rodauth::Rails::Auth
845
+ configure do
846
+ session_key_prefix "admin_"
847
+ end
848
+ end
849
+ ```
850
+ ```rb
851
+ # in a controller test:
852
+ session[:admin_account_id] = account.id
853
+ session[:admin_authenticated_by] = ["password"]
854
+ ```
855
+
856
+ If you want to access the Rodauth instance in controller tests, you can do so
857
+ through the controller instance:
858
+
859
+ ```rb
860
+ # in a controller test:
861
+ @controller.rodauth #=> #<RodauthMain ...>
862
+ @controller.rodauth(:admin) #=> #<RodauthAdmin ...>
863
+ ```
864
+
786
865
  ## Configuring
787
866
 
788
867
  ### Configuration methods
@@ -3,23 +3,18 @@ enable_extension "citext"
3
3
 
4
4
  <% end -%>
5
5
  create_table :accounts<%= primary_key_type %> do |t|
6
+ t.integer :status, null: false, default: 1
6
7
  <% case activerecord_adapter -%>
7
8
  <% when "postgresql" -%>
8
9
  t.citext :email, null: false
9
10
  <% else -%>
10
11
  t.string :email, null: false
11
12
  <% end -%>
12
- t.integer :status, null: false, default: 1
13
13
  <% case activerecord_adapter -%>
14
14
  <% when "postgresql", "sqlite3" -%>
15
15
  t.index :email, unique: true, where: "status IN (1, 2)"
16
16
  <% else -%>
17
17
  t.index :email, unique: true
18
18
  <% end -%>
19
- end
20
-
21
- # Used if storing password hashes in a separate table (default)
22
- create_table :account_password_hashes<%= primary_key_type %> do |t|
23
- t.foreign_key :accounts, column: :id
24
- t.string :password_hash, null: false
19
+ t.string :password_hash
25
20
  end
@@ -35,7 +35,7 @@ class RodauthMain < Rodauth::Rails::Auth
35
35
  account_status_column :status
36
36
 
37
37
  # Store password hash in a column instead of a separate table.
38
- # account_password_hash_column :password_digest
38
+ account_password_hash_column :password_hash
39
39
 
40
40
  # Set password when creating account instead of when verifying.
41
41
  verify_account_set_password? false
@@ -138,6 +138,9 @@ class RodauthMain < Rodauth::Rails::Auth
138
138
 
139
139
  # Redirect to login page after password reset.
140
140
  reset_password_redirect { login_path }
141
+
142
+ # Ensure requiring login follows login route changes.
143
+ require_login_redirect { login_path }
141
144
  <% end -%>
142
145
 
143
146
  # ==> Deadlines
@@ -1,4 +1,8 @@
1
1
  class Account < ApplicationRecord
2
2
  include Rodauth::Rails.model
3
+ <% if ActiveRecord.version >= Gem::Version.new("7.0") -%>
3
4
  enum :status, unverified: 1, verified: 2, closed: 3
5
+ <% else -%>
6
+ enum status: { unverified: 1, verified: 2, closed: 3 }
7
+ <% end -%>
4
8
  end
@@ -5,17 +5,21 @@ module Rodauth
5
5
  module Rails
6
6
  # The superclass for creating a Rodauth middleware.
7
7
  class App < Roda
8
- require "rodauth/rails/app/middleware"
9
- plugin Middleware
8
+ plugin :middleware, forward_response_headers: true do |middleware|
9
+ middleware.class_eval do
10
+ def self.inspect
11
+ "#{superclass}::Middleware"
12
+ end
13
+
14
+ def inspect
15
+ "#<#{self.class.inspect} request=#{request.inspect} response=#{response.inspect}>"
16
+ end
17
+ end
18
+ end
10
19
 
11
20
  plugin :hooks
12
21
  plugin :render, layout: false
13
22
 
14
- unless Rodauth::Rails.api_only?
15
- require "rodauth/rails/app/flash"
16
- plugin Flash
17
- end
18
-
19
23
  def self.configure(*args, **options, &block)
20
24
  auth_class = args.shift if args[0].is_a?(Class)
21
25
  name = args.shift if args[0].is_a?(Symbol)
@@ -35,6 +39,14 @@ module Rodauth
35
39
  end
36
40
  end
37
41
 
42
+ after do
43
+ rails_request.commit_flash
44
+ end unless ActionPack.version < Gem::Version.new("5.0")
45
+
46
+ def flash
47
+ rails_request.flash
48
+ end
49
+
38
50
  def rails_routes
39
51
  ::Rails.application.routes.url_helpers
40
52
  end
@@ -18,6 +18,15 @@ module Rodauth
18
18
 
19
19
  private
20
20
 
21
+ # Adds response status to instrumentation payload for logging,
22
+ # when calling a halting rodauth method inside a controller.
23
+ def append_info_to_payload(payload)
24
+ super
25
+ if request.env["rodauth.rails.status"]
26
+ payload[:status] = request.env.delete("rodauth.rails.status")
27
+ end
28
+ end
29
+
21
30
  def rodauth_response
22
31
  res = catch(:halt) { return yield }
23
32
 
@@ -54,6 +54,16 @@ module Rodauth
54
54
 
55
55
  private
56
56
 
57
+ unless ActionPack.version < Gem::Version.new("5.0")
58
+ # When calling a Rodauth method that redirects inside the Rails
59
+ # router, Roda's after hook that commits the flash would never get
60
+ # called, so we make sure to commit the flash beforehand.
61
+ def redirect(*)
62
+ rails_request.commit_flash
63
+ super
64
+ end
65
+ end
66
+
57
67
  def instantiate_rails_account
58
68
  if defined?(ActiveRecord::Base) && rails_account_model < ActiveRecord::Base
59
69
  rails_account_model.instantiate(account.stringify_keys)
@@ -10,6 +10,14 @@ module Rodauth
10
10
 
11
11
  def redirect(*)
12
12
  rails_instrument_redirection { super }
13
+ ensure
14
+ request.env["rodauth.rails.status"] = response.status
15
+ end
16
+
17
+ def return_response(*)
18
+ super
19
+ ensure
20
+ request.env["rodauth.rails.status"] = response.status
13
21
  end
14
22
 
15
23
  def rails_render(*)
@@ -1,5 +1,6 @@
1
1
  require "rodauth/rails/middleware"
2
2
  require "rodauth/rails/controller_methods"
3
+ require "rodauth/rails/test"
3
4
 
4
5
  require "rails"
5
6
 
@@ -21,6 +22,14 @@ module Rodauth
21
22
  initializer "rodauth.test" do
22
23
  # Rodauth uses RACK_ENV to set the default bcrypt hash cost
23
24
  ENV["RACK_ENV"] = "test" if ::Rails.env.test?
25
+
26
+ if ActionPack.version >= Gem::Version.new("5.0")
27
+ ActiveSupport.on_load(:action_controller_test_case) do
28
+ include Rodauth::Rails::Test::Controller
29
+ end
30
+ else
31
+ ActionController::TestCase.include Rodauth::Rails::Test::Controller
32
+ end
24
33
  end
25
34
 
26
35
  rake_tasks do
@@ -0,0 +1,41 @@
1
+ require "active_support/concern"
2
+
3
+ module Rodauth
4
+ module Rails
5
+ module Test
6
+ module Controller
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ setup :setup_rodauth
11
+ end
12
+
13
+ def process(*)
14
+ catch_rodauth { super }
15
+ end
16
+ ruby2_keywords(:process) if respond_to?(:ruby2_keywords, true)
17
+
18
+ private
19
+
20
+ def setup_rodauth
21
+ Rodauth::Rails.app.opts[:rodauths].each do |name, auth_class|
22
+ scope = auth_class.roda_class.new(request.env)
23
+ request.env[["rodauth", *name].join(".")] = auth_class.new(scope)
24
+ end
25
+ end
26
+
27
+ def catch_rodauth(&block)
28
+ result = catch(:halt, &block)
29
+
30
+ if result.is_a?(Array) # rodauth response
31
+ response.status = result[0]
32
+ response.headers.merge! result[1]
33
+ response.body = result[2]
34
+ end
35
+
36
+ response
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,7 @@
1
+ module Rodauth
2
+ module Rails
3
+ module Test
4
+ autoload :Controller, "rodauth/rails/test/controller"
5
+ end
6
+ end
7
+ end
@@ -1,5 +1,5 @@
1
1
  module Rodauth
2
2
  module Rails
3
- VERSION = "1.2.1"
3
+ VERSION = "1.3.1"
4
4
  end
5
5
  end
@@ -17,7 +17,8 @@ Gem::Specification.new do |spec|
17
17
  spec.require_paths = ["lib"]
18
18
 
19
19
  spec.add_dependency "railties", ">= 4.2", "< 8"
20
- spec.add_dependency "rodauth", "~> 2.19"
20
+ spec.add_dependency "rodauth", "~> 2.23"
21
+ spec.add_dependency "roda", "~> 3.55"
21
22
  spec.add_dependency "sequel-activerecord_connection", "~> 1.1"
22
23
  spec.add_dependency "tilt"
23
24
  spec.add_dependency "bcrypt"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rodauth-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.1
4
+ version: 1.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Janko Marohnić
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-02-19 00:00:00.000000000 Z
11
+ date: 2022-04-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: railties
@@ -36,14 +36,28 @@ dependencies:
36
36
  requirements:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: '2.19'
39
+ version: '2.23'
40
40
  type: :runtime
41
41
  prerelease: false
42
42
  version_requirements: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: '2.19'
46
+ version: '2.23'
47
+ - !ruby/object:Gem::Dependency
48
+ name: roda
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.55'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '3.55'
47
61
  - !ruby/object:Gem::Dependency
48
62
  name: sequel-activerecord_connection
49
63
  requirement: !ruby/object:Gem::Requirement
@@ -230,8 +244,6 @@ files:
230
244
  - lib/rodauth-rails.rb
231
245
  - lib/rodauth/rails.rb
232
246
  - lib/rodauth/rails/app.rb
233
- - lib/rodauth/rails/app/flash.rb
234
- - lib/rodauth/rails/app/middleware.rb
235
247
  - lib/rodauth/rails/auth.rb
236
248
  - lib/rodauth/rails/controller_methods.rb
237
249
  - lib/rodauth/rails/feature.rb
@@ -247,6 +259,8 @@ files:
247
259
  - lib/rodauth/rails/model/associations.rb
248
260
  - lib/rodauth/rails/railtie.rb
249
261
  - lib/rodauth/rails/tasks.rake
262
+ - lib/rodauth/rails/test.rb
263
+ - lib/rodauth/rails/test/controller.rb
250
264
  - lib/rodauth/rails/version.rb
251
265
  - rodauth-rails.gemspec
252
266
  homepage: https://github.com/janko/rodauth-rails
@@ -1,46 +0,0 @@
1
- module Rodauth
2
- module Rails
3
- class App
4
- # Roda plugin that sets up Rails flash integration.
5
- module Flash
6
- def self.load_dependencies(app)
7
- app.plugin :hooks
8
- end
9
-
10
- def self.configure(app)
11
- app.before { request.flash } # load flash
12
- app.after { request.commit_flash } # save flash
13
- end
14
-
15
- module InstanceMethods
16
- def flash
17
- request.flash
18
- end
19
- end
20
-
21
- module RequestMethods
22
- # If the redirect would bubble up outside of the Roda app, the after
23
- # hook would never get called, so we make sure to commit the flash.
24
- def redirect(*)
25
- commit_flash
26
- super
27
- end
28
-
29
- def flash
30
- scope.rails_request.flash
31
- end
32
-
33
- if ActionPack.version >= Gem::Version.new("5.0")
34
- def commit_flash
35
- scope.rails_request.commit_flash
36
- end
37
- else
38
- def commit_flash
39
- # ActionPack 4.2 automatically commits flash
40
- end
41
- end
42
- end
43
- end
44
- end
45
- end
46
- end
@@ -1,36 +0,0 @@
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.configure(app)
7
- handle_result = -> (env, res) do
8
- if headers = env.delete("rodauth.rails.headers")
9
- res[1] = headers.merge(res[1])
10
- end
11
- end
12
-
13
- app.plugin :middleware, handle_result: handle_result do |middleware|
14
- middleware.plugin :hooks
15
-
16
- middleware.after do
17
- if response.empty? && response.headers.any?
18
- env["rodauth.rails.headers"] = response.headers
19
- end
20
- end
21
-
22
- middleware.class_eval do
23
- def self.inspect
24
- "#{superclass}::Middleware"
25
- end
26
-
27
- def inspect
28
- "#<#{self.class.inspect} request=#{request.inspect} response=#{response.inspect}>"
29
- end
30
- end
31
- end
32
- end
33
- end
34
- end
35
- end
36
- end