rodauth-rails 0.7.0 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8163d64892cbebd867182d15148f3099abb3ed49ae3e07a89a5adea6606623d2
4
- data.tar.gz: 3cc7990e0af8e5ffb2ac959f989fb45cf538490412adfc908571823e5dd7b160
3
+ metadata.gz: 0f1312dbd1bb4dc0d954c77a5ff350b5c9e1ff3fc4dd45b8834cd3e7d0280a22
4
+ data.tar.gz: 5dda5720126361589a428add9b8256b35aa53644166ca7a8a6d14c5baef53f02
5
5
  SHA512:
6
- metadata.gz: 99005d6864310fa3a36f8314a13588900a5ac1559af7a77d75cb5aba66b0b829d32c83fe66a3f5a7ced098de32b39396edd666919177836bb84b35a0de3a558b
7
- data.tar.gz: 2d66b5ab43d05b26483cb3d69181c506b19a937fa77a2d7d66a38708f6357fae7bd2e605cc0a96affdd8fed822076dccb1603577338e68620c70a816fc45db7a
6
+ metadata.gz: f70e5a44db25c016fe92169be342d1f489cd0e3307fe6c06dbe822c28c05f55dc696b26721836d315daabbbfb0889d18357cec3bb7aa52932649f5ecb08ceedb
7
+ data.tar.gz: 6af3cd43f9266729049d984c9da58beff019dd2f0148465d65c8f814602d9a9678308d752ef469f08ab381a1373b919d1e61b62b204751d95ca57d10ed05de2a
@@ -1,3 +1,9 @@
1
+ ## 0.8.0 (2021-01-03)
2
+
3
+ * Add `--api` option to `rodauth:install` generator for choosing JSON-only configuration (@janko)
4
+
5
+ * Don't blow up when a Rodauth request is made using an unsupported HTTP verb (@janko)
6
+
1
7
  ## 0.7.0 (2020-11-27)
2
8
 
3
9
  * Add `#rails_controller_eval` method for running code in context of a controller instance (@janko)
data/README.md CHANGED
@@ -12,7 +12,25 @@ Useful links:
12
12
  Articles:
13
13
 
14
14
  * [Rodauth: A Refreshing Authentication Solution for Ruby](https://janko.io/rodauth-a-refreshing-authentication-solution-for-ruby/)
15
- * [Adding Authentication in Rails 6 with Rodauth](https://janko.io/adding-authentication-in-rails-with-rodauth/)
15
+ * [Adding Authentication in Rails with Rodauth](https://janko.io/adding-authentication-in-rails-with-rodauth/)
16
+ * [Adding Multifactor Authentication in Rails with Rodauth](https://janko.io/adding-multifactor-authentication-in-rails-with-rodauth/)
17
+
18
+ ## Why Rodauth?
19
+
20
+ There are already several popular authentication solutions for Rails (Devise,
21
+ Sorcery, Clearance, Authlogic), so why would you choose Rodauth? Well, because
22
+ it has many advantages over the mentioned alternatives:
23
+
24
+ * multifactor authentication ([TOTP][otp], [SMS codes][sms_codes], [recovery codes][recovery_codes], [WebAuthn][webauthn])
25
+ * standardized [JSON API support][jwt] (for every feature)
26
+ * enterprise security features ([password complexity][password_complexity], [disallow password reuse][disallow_password_reuse], [password expiration][password_expiration], [session expiration][session_expiration], [single session][single_session], [account expiration][account_expiration])
27
+ * [email authentication][email_auth] (aka "passwordless")
28
+ * [audit logging][audit_logging] (for any action)
29
+ * ability to protect password hashes even in case of SQL injection ([more details][password protection])
30
+ * additional bruteforce protection for tokens ([more details][bruteforce tokens])
31
+ * uniform configuration DSL (any setting can be static or dynamic)
32
+ * consistent before/after hooks around everything
33
+ * dedicated object encapsulating all authentication logic
16
34
 
17
35
  ## Upgrading
18
36
 
@@ -21,14 +39,14 @@ Articles:
21
39
  Starting from version 0.7.0, rodauth-rails now correctly detects Rails
22
40
  application's `secret_key_base` when setting default `hmac_secret`, including
23
41
  when it's set via credentials or `$SECRET_KEY_BASE` environment variable. This
24
- means authentication will be more secure by default, and Rodauth features that
25
- require `hmac_secret` should now work automatically as well.
42
+ means that your authentication will now be more secure by default, and Rodauth
43
+ features that require `hmac_secret` should now work automatically as well.
26
44
 
27
45
  However, if you've already been using rodauth-rails in production, where the
28
46
  `secret_key_base` is set via credentials or environment variable and `hmac_secret`
29
47
  was not explicitly set, the fact that your authentication will now start using
30
48
  HMACs has backwards compatibility considerations. See the [Rodauth
31
- documentation](hmac) for instructions on how to safely transition, or just set
49
+ documentation][hmac] for instructions on how to safely transition, or just set
32
50
  `hmac_secret nil` in your Rodauth configuration.
33
51
 
34
52
  ## Installation
@@ -48,10 +66,17 @@ Then run `bundle install`.
48
66
 
49
67
  Next, run the install generator:
50
68
 
51
- ```
69
+ ```sh
52
70
  $ rails generate rodauth:install
53
71
  ```
54
72
 
73
+ Or if you want Rodauth endpoints to be exposed via JSON API:
74
+
75
+ ```sh
76
+ $ rails generate rodauth:install --api
77
+ $ bundle add jwt
78
+ ```
79
+
55
80
  The generator will create the following files:
56
81
 
57
82
  * Rodauth migration at `db/migrate/*_create_rodauth.rb`
@@ -185,14 +210,12 @@ Using this information, we could add some basic authentication links to our
185
210
  navigation header:
186
211
 
187
212
  ```erb
188
- <ul>
189
- <% if rodauth.authenticated? %>
190
- <li><%= link_to "Sign out", rodauth.logout_path, method: :post %></li>
191
- <% else %>
192
- <li><%= link_to "Sign in", rodauth.login_path %></li>
193
- <li><%= link_to "Sign up", rodauth.create_account_path %></li>
194
- <% end %>
195
- </ul>
213
+ <% if rodauth.logged_in? %>
214
+ <%= link_to "Sign out", rodauth.logout_path, method: :post %>
215
+ <% else %>
216
+ <%= link_to "Sign in", rodauth.login_path %>
217
+ <%= link_to "Sign up", rodauth.create_account_path %>
218
+ <% end %>
196
219
  ```
197
220
 
198
221
  These routes are fully functional, feel free to visit them and interact with the
@@ -208,7 +231,7 @@ retrieves the corresponding account record:
208
231
  ```rb
209
232
  # app/controllers/application_controller.rb
210
233
  class ApplicationController < ActionController::Base
211
- before_action :current_account, if: -> { rodauth.authenticated? }
234
+ before_action :current_account, if: -> { rodauth.logged_in? }
212
235
 
213
236
  private
214
237
 
@@ -382,7 +405,7 @@ $ rails generate rodauth:mailer
382
405
  ```
383
406
 
384
407
  This will create a `RodauthMailer` with the associated mailer views in
385
- `app/views/rodauth_mailer` directory.
408
+ `app/views/rodauth_mailer` directory:
386
409
 
387
410
  ```rb
388
411
  # app/mailers/rodauth_mailer.rb
@@ -434,9 +457,9 @@ end
434
457
  ```
435
458
 
436
459
  This approach can be used even if you're using a 3rd-party service for
437
- transactional emails, where emails are sent via API requests instead of
438
- SMTP. Whatever the `create_*_email` block returns will be passed to
439
- `send_email`, so you can be creative.
460
+ transactional emails, where emails are sent via HTTP instead of SMTP. Whatever
461
+ the `create_*_email` block returns will be passed to `send_email`, so you can
462
+ be creative.
440
463
 
441
464
  ### Migrations
442
465
 
@@ -458,37 +481,6 @@ class CreateRodauthOtpSmsCodesRecoveryCodes < ActiveRecord::Migration
458
481
  end
459
482
  ```
460
483
 
461
- ### JSON API
462
-
463
- JSON API support in Rodauth is provided by the [JWT feature]. First you'll need
464
- to add the [JWT gem] to your Gemfile:
465
-
466
- ```rb
467
- gem "jwt"
468
- ```
469
-
470
- The following configuration will enable the Rodauth endpoints to be accessed
471
- via JSON requests (in addition to HTML requests):
472
-
473
- ```rb
474
- # app/lib/rodauth_app.rb
475
- class RodauthApp < Rodauth::Rails::App
476
- configure(json: true) do
477
- # ...
478
- enable :jwt
479
- jwt_secret "...your secret key..."
480
- # ...
481
- end
482
- end
483
- ```
484
-
485
- If you want the endpoints to be only accessible via JSON requests, or if your
486
- Rails app is in API-only mode, instead of `json: true` pass `json: :only` to
487
- the configure method.
488
-
489
- Make sure to store the `jwt_secret` in a secure place, such as Rails
490
- credentials or environment variables.
491
-
492
484
  ### Calling controller methods
493
485
 
494
486
  When using Rodauth before/after hooks or generally overriding your Rodauth
@@ -585,13 +577,38 @@ integration for Rodauth:
585
577
  * runs Action Controller callbacks & rescue handlers around Rodauth actions
586
578
  * uses Action Mailer for sending emails
587
579
 
588
- The `configure { ... }` method wraps configuring the Rodauth plugin, forwarding
580
+ The `configure` method wraps configuring the Rodauth plugin, forwarding
589
581
  any additional [plugin options].
590
582
 
591
583
  ```rb
592
- configure { ... } # defining default Rodauth configuration
593
- configure(json: true) { ... } # passing options to the Rodauth plugin
594
- configure(:secondary) { ... } # defining multiple Rodauth configurations
584
+ class RodauthApp < Rodauth::Rails::App
585
+ configure { ... } # defining default Rodauth configuration
586
+ configure(json: true) { ... } # passing options to the Rodauth plugin
587
+ configure(:secondary) { ... } # defining multiple Rodauth configurations
588
+ end
589
+ ```
590
+
591
+ The `route` block is provided by Roda, and it's called on each request before
592
+ it reaches the Rails router.
593
+
594
+ ```rb
595
+ class RodauthApp < Rodauth::Rails::App
596
+ route do |r|
597
+ # ... called before each request ...
598
+ end
599
+ end
600
+ ```
601
+
602
+ Since `Rodauth::Rails::App` is just a Roda subclass, you can do anything you
603
+ would with a Roda app, such as loading additional Roda plugins:
604
+
605
+ ```rb
606
+ class RodauthApp < Rodauth::Rails::App
607
+ plugin :request_headers # easier access to request headers
608
+ plugin :typecast_params # methods for conversion of request params
609
+ plugin :default_headers, { "Foo" => "Bar" }
610
+ # ...
611
+ end
595
612
  ```
596
613
 
597
614
  ### Sequel
@@ -607,6 +624,142 @@ connection (using the [sequel-activerecord_connection] gem).
607
624
  This means that, from the usage perspective, Sequel can be considered just
608
625
  as an implementation detail of Rodauth.
609
626
 
627
+ ## JSON API
628
+
629
+ JSON API support in Rodauth is provided by the [JWT feature][jwt]. You'll need
630
+ to install the [JWT gem], enable JSON support and enable the JWT feature:
631
+
632
+ ```sh
633
+ $ bundle add jwt
634
+ ```
635
+ ```rb
636
+ # app/lib/rodauth_app.rb
637
+ class RodauthApp < Rodauth::Rails::App
638
+ configure(json: :only) do
639
+ # ...
640
+ enable :jwt
641
+ # make sure to store the JWT secret below in a safe place
642
+ jwt_secret "...your secret key..."
643
+ # ...
644
+ end
645
+ end
646
+ ```
647
+
648
+ With the above configuration, Rodauth routes will only be accessible via JSON
649
+ requests. If you still want to allow HTML access alongside JSON, change `json:
650
+ :only` to `json: true`.
651
+
652
+ Emails will automatically work in JSON-only mode, because `Rodauth::Rails::App`
653
+ comes with Roda's `render` plugin loaded. They are customized the same as in
654
+ the non-JSON case.
655
+
656
+ ## OmniAuth
657
+
658
+ While Rodauth doesn't yet come with [OmniAuth] integration, we can build one
659
+ ourselves using the existing Rodauth API.
660
+
661
+ In order to allow the user to login via multiple external providers, let's
662
+ create an `account_identities` table that will have a many-to-one relationship
663
+ with the `accounts` table:
664
+
665
+ ```sh
666
+ $ rails generate model AccountIdentity
667
+ ```
668
+ ```rb
669
+ # db/migrate/*_create_account_identities.rb
670
+ class CreateAccountIdentities < ActiveRecord::Migration
671
+ def change
672
+ create_table :account_identities do |t|
673
+ t.references :account, null: false, foreign_key: { on_delete: :cascade }
674
+ t.string :provider, null: false
675
+ t.string :uid, null: false
676
+ t.jsonb :info, null: false, default: {} # adjust JSON column type for your database
677
+
678
+ t.timestamps
679
+
680
+ t.index [:provider, :uid], unique: true
681
+ end
682
+ end
683
+ end
684
+ ```
685
+ ```rb
686
+ # app/models/account_identity.rb
687
+ class AcccountIdentity < ApplicationRecord
688
+ belongs_to :account
689
+ end
690
+ ```
691
+ ```rb
692
+ # app/models/account.rb
693
+ class Account < ApplicationRecord
694
+ has_many :identities, class_name: "AccountIdentity"
695
+ end
696
+ ```
697
+
698
+ Let's assume we want to implement Facebook login, and have added the
699
+ corresponding OmniAuth strategy to the middleware stack, together with an
700
+ authorization link on the login form:
701
+
702
+ ```rb
703
+ Rails.application.config.middleware.use OmniAuth::Builder do
704
+ provider :facebook, ENV["FACEBOOK_APP_ID"], ENV["FACEBOOK_APP_SECRET"],
705
+ scope: "email", callback_path: "/auth/facebook/callback"
706
+ end
707
+ ```
708
+ ```erb
709
+ <%= link_to "Login via Facebook", "/auth/facebook" %>
710
+ ```
711
+
712
+ Let's implement the OmniAuth callback endpoint on our Rodauth controller:
713
+
714
+ ```rb
715
+ # config/routes.rb
716
+ Rails.application.routes.draw do
717
+ # ...
718
+ get "/auth/:provider/callback", to: "rodauth#omniauth"
719
+ end
720
+ ```
721
+ ```rb
722
+ # app/controllres/rodauth_controller.rb
723
+ class RodauthController < ApplicationController
724
+ def omniauth
725
+ auth = request.env["omniauth.auth"]
726
+
727
+ # attempt to find existing identity directly
728
+ identity = AccountIdentity.find_by(provider: auth["provider"], uid: auth["uid"])
729
+
730
+ if identity
731
+ # update any external info changes
732
+ identity.update!(info: auth["info"])
733
+ # set account from identity
734
+ account = identity.account
735
+ end
736
+
737
+ # attempt to find an existing account by email
738
+ account ||= Account.find_by(email: auth["info"]["email"])
739
+
740
+ # disallow login if account is not verified
741
+ if account && account.status != rodauth.account_open_status_value
742
+ redirect_to rodauth.login_path, alert: rodauth.unverified_account_message
743
+ return
744
+ end
745
+
746
+ # create new account if it doesn't exist
747
+ unless account
748
+ account = Account.create!(email: auth["info"]["email"])
749
+ end
750
+
751
+ # create new identity if it doesn't exist
752
+ unless identity
753
+ account.identities.create!(provider: auth["provider"], uid: auth["uid"], info: auth["info"])
754
+ end
755
+
756
+ # login with Rodauth
757
+ rodauth.account_from_login(account.email)
758
+ rodauth.login("omniauth")
759
+ end
760
+ end
761
+ ```
762
+
610
763
  ## Configuring
611
764
 
612
765
  For the list of configuration methods provided by Rodauth, see the [feature
@@ -640,6 +793,37 @@ Rodauth::Rails.configure do |config|
640
793
  end
641
794
  ```
642
795
 
796
+ ## Custom extensions
797
+
798
+ When developing custom extensions for Rodauth inside your Rails project, it's
799
+ better to use plain modules (at least in the beginning), because Rodauth
800
+ feature API doesn't yet support Zeitwerk reloading well.
801
+
802
+ ```rb
803
+ # app/lib/rodauth_argon2.rb
804
+ module RodauthArgon2
805
+ def password_hash(password)
806
+ Argon2::Password.create(password, t_cost: password_hash_cost, m_cost: password_hash_cost)
807
+ end
808
+
809
+ def password_hash_match?(hash, password)
810
+ Argon2::Password.verify_password(password, hash)
811
+ end
812
+ end
813
+ ```
814
+ ```rb
815
+ # app/lib/rodauth_app.rb
816
+ class RodauthApp < Rodauth::Rails::App
817
+ configure do
818
+ # ...
819
+ auth_class_eval do
820
+ include RodauthArgon2
821
+ end
822
+ # ...
823
+ end
824
+ end
825
+ ```
826
+
643
827
  ## Testing
644
828
 
645
829
  If you're writing system tests, it's generally better to go through the actual
@@ -712,6 +896,8 @@ Rodauth method for creating database functions:
712
896
 
713
897
  ```rb
714
898
  # db/migrate/*_create_rodauth_database_functions.rb
899
+ require "rodauth/migrations"
900
+
715
901
  class CreateRodauthDatabaseFunctions < ActiveRecord::Migration
716
902
  def up
717
903
  Rodauth.create_database_authentication_functions(DB)
@@ -776,7 +962,6 @@ conduct](https://github.com/janko/rodauth-rails/blob/master/CODE_OF_CONDUCT.md).
776
962
  [Rodauth]: https://github.com/jeremyevans/rodauth
777
963
  [Sequel]: https://github.com/jeremyevans/sequel
778
964
  [feature documentation]: http://rodauth.jeremyevans.net/documentation.html
779
- [JWT feature]: http://rodauth.jeremyevans.net/rdoc/files/doc/jwt_rdoc.html
780
965
  [JWT gem]: https://github.com/jwt/ruby-jwt
781
966
  [Bootstrap]: https://getbootstrap.com/
782
967
  [Roda]: http://roda.jeremyevans.net/
@@ -786,3 +971,19 @@ conduct](https://github.com/janko/rodauth-rails/blob/master/CODE_OF_CONDUCT.md).
786
971
  [sequel-activerecord_connection]: https://github.com/janko/sequel-activerecord_connection
787
972
  [plugin options]: http://rodauth.jeremyevans.net/rdoc/files/README_rdoc.html#label-Plugin+Options
788
973
  [hmac]: http://rodauth.jeremyevans.net/rdoc/files/README_rdoc.html#label-HMAC
974
+ [OmniAuth]: https://github.com/omniauth/omniauth
975
+ [otp]: http://rodauth.jeremyevans.net/rdoc/files/doc/otp_rdoc.html
976
+ [sms_codes]: http://rodauth.jeremyevans.net/rdoc/files/doc/sms_codes_rdoc.html
977
+ [recovery_codes]: http://rodauth.jeremyevans.net/rdoc/files/doc/recovery_codes_rdoc.html
978
+ [webauthn]: http://rodauth.jeremyevans.net/rdoc/files/doc/webauthn_rdoc.html
979
+ [jwt]: http://rodauth.jeremyevans.net/rdoc/files/doc/jwt_rdoc.html
980
+ [email_auth]: http://rodauth.jeremyevans.net/rdoc/files/doc/email_auth_rdoc.html
981
+ [audit_logging]: http://rodauth.jeremyevans.net/rdoc/files/doc/audit_logging_rdoc.html
982
+ [password protection]: https://github.com/jeremyevans/rodauth#label-Password+Hash+Access+Via+Database+Functions
983
+ [bruteforce tokens]: https://github.com/jeremyevans/rodauth#label-Tokens
984
+ [password_complexity]: http://rodauth.jeremyevans.net/rdoc/files/doc/password_complexity_rdoc.html
985
+ [disallow_password_reuse]: http://rodauth.jeremyevans.net/rdoc/files/doc/disallow_password_reuse_rdoc.html
986
+ [password_expiration]: http://rodauth.jeremyevans.net/rdoc/files/doc/password_expiration_rdoc.html
987
+ [session_expiration]: http://rodauth.jeremyevans.net/rdoc/files/doc/session_expiration_rdoc.html
988
+ [single_session]: http://rodauth.jeremyevans.net/rdoc/files/doc/single_session_rdoc.html
989
+ [account_expiration]: http://rodauth.jeremyevans.net/rdoc/files/doc/account_expiration_rdoc.html
@@ -13,6 +13,15 @@ module Rodauth
13
13
  source_root "#{__dir__}/templates"
14
14
  namespace "rodauth:install"
15
15
 
16
+ # The :api option is a Rails-recognized option that always
17
+ # defaults to false, so we make it use our provided default
18
+ # value instead.
19
+ def self.default_value_for_option(name, options)
20
+ name == :api ? options[:default] : super
21
+ end
22
+
23
+ class_option :api, type: :boolean, desc: "Generate JSON-only configuration"
24
+
16
25
  def create_rodauth_migration
17
26
  return unless defined?(ActiveRecord::Base)
18
27
 
@@ -75,9 +84,11 @@ module Rodauth
75
84
  end
76
85
 
77
86
  def api_only?
78
- return unless ::Rails.gem_version >= Gem::Version.new("5.0")
79
-
80
- ::Rails.application.config.api_only
87
+ if options.key?(:api)
88
+ options[:api]
89
+ elsif ::Rails.gem_version >= Gem::Version.new("5.0")
90
+ ::Rails.application.config.api_only
91
+ end
81
92
  end
82
93
 
83
94
  def migration_features
@@ -1,4 +1,6 @@
1
1
  require "roda"
2
+ require "rodauth"
3
+ require "rodauth/rails/feature"
2
4
 
3
5
  module Rodauth
4
6
  module Rails
@@ -68,9 +68,11 @@ module Rodauth
68
68
 
69
69
  if rails_controller_instance.performed?
70
70
  rails_controller_response
71
- else
71
+ elsif result
72
72
  result[1].merge!(rails_controller_instance.response.headers)
73
73
  throw :halt, result
74
+ else
75
+ result
74
76
  end
75
77
  end
76
78
 
@@ -1,5 +1,5 @@
1
1
  module Rodauth
2
2
  module Rails
3
- VERSION = "0.7.0"
3
+ VERSION = "0.8.0"
4
4
  end
5
5
  end
@@ -17,7 +17,7 @@ Gem::Specification.new do |spec|
17
17
  spec.require_paths = ["lib"]
18
18
 
19
19
  spec.add_dependency "railties", ">= 4.2", "< 7"
20
- spec.add_dependency "rodauth", "~> 2.6"
20
+ spec.add_dependency "rodauth", "~> 2.7"
21
21
  spec.add_dependency "sequel-activerecord_connection", "~> 1.1"
22
22
  spec.add_dependency "tilt"
23
23
  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: 0.7.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Janko Marohnić
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-11-27 00:00:00.000000000 Z
11
+ date: 2021-01-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: railties
@@ -36,14 +36,14 @@ dependencies:
36
36
  requirements:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: '2.6'
39
+ version: '2.7'
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.6'
46
+ version: '2.7'
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: sequel-activerecord_connection
49
49
  requirement: !ruby/object:Gem::Requirement
@@ -187,7 +187,6 @@ files:
187
187
  - lib/generators/rodauth/templates/db/migrate/create_rodauth.rb
188
188
  - lib/generators/rodauth/views_generator.rb
189
189
  - lib/rodauth-rails.rb
190
- - lib/rodauth/features/rails.rb
191
190
  - lib/rodauth/rails.rb
192
191
  - lib/rodauth/rails/app.rb
193
192
  - lib/rodauth/rails/app/flash.rb
@@ -1 +0,0 @@
1
- require "rodauth/rails/feature"