rodauth-rails 0.14.0 → 0.15.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d447d09fef8c29feb6240523286b8906049e85965f20a6410d1a475f913d9051
4
- data.tar.gz: bca9b6eadec6b32f2193291c6922467a554105d290ffd7b34bc2606d62121926
3
+ metadata.gz: '097625662e9cefbf7484ea775c9b1930968fbbc3a1b8ad24df9e229e2194e301'
4
+ data.tar.gz: fbbfe9dd849646e859aecbf3c600168fc0b65255f7b2bf933a2e42591f8b0b79
5
5
  SHA512:
6
- metadata.gz: 1f512f9fe9a3e22dcddf477d8906d1ea63a548241fd93b43bbcaf274ff39e0104e20f64c6a2836e5b243e812ffde654deae55a0beca69f4ba917cd5943da8a3c
7
- data.tar.gz: dbbd99959dfd42134cd3374f1f9767cf3e8d49327c195d4c35c4ecf281d0c3dad52db76b7e2fbf030c9e3ea2131bfcb1b6a120cc4a310983d8db564e63b97cda
6
+ metadata.gz: 762d0c1725dcd0017cdd6722894e546dfdf246e245af688a8bc99f177e43765fe8bd7a79639e45d3146ca5bedadce9f34389f61bf3a6957b406cbc664cf93829
7
+ data.tar.gz: 8c6624c70668356b8434dde9bd237cced89f23d4d241b770989f1af68ee687b7186f305ada409885d619b9974e7d2d47b6fddc9faf6935653cab805ee8709167
data/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## 0.15.0 (2021-07-29)
2
+
3
+ * Add `Rodauth::Rails::Model` mixin that defines password attribute and associations on the model (@janko)
4
+
5
+ * Add support for the new internal_request feature (@janko)
6
+
7
+ * Implement `Rodauth::Rails.rodauth` in terms of the internal_request feature (@janko)
8
+
1
9
  ## 0.14.0 (2021-07-10)
2
10
 
3
11
  * Speed up template rendering by only searching formats accepted by the request (@janko)
data/README.md CHANGED
@@ -49,7 +49,7 @@ For instructions on upgrading from previous rodauth-rails versions, see
49
49
  Add the gem to your Gemfile:
50
50
 
51
51
  ```rb
52
- gem "rodauth-rails", "~> 0.14"
52
+ gem "rodauth-rails", "~> 0.15"
53
53
 
54
54
  # gem "jwt", require: false # for JWT feature
55
55
  # gem "rotp", require: false # for OTP feature
@@ -453,6 +453,130 @@ class CreateRodauthOtpSmsCodesRecoveryCodes < ActiveRecord::Migration
453
453
  end
454
454
  ```
455
455
 
456
+ ### Model
457
+
458
+ The `Rodauth::Rails::Model` mixin can be included into the account model, which
459
+ defines a password attribute and associations for tables used by enabled
460
+ authentication features.
461
+
462
+ ```rb
463
+ class Account < ApplicationRecord
464
+ include Rodauth::Rails.model # or `Rodauth::Rails.model(:admin)`
465
+ end
466
+ ```
467
+
468
+ #### Password attribute
469
+
470
+ Regardless of whether you're storing the password hash in a column in the
471
+ accounts table, or in a separate table, the `#password` attribute can be used
472
+ to set or clear the password hash.
473
+
474
+ ```rb
475
+ account = Account.create!(email: "user@example.com", password: "secret")
476
+
477
+ # when password hash is stored in a column on the accounts table
478
+ account.password_hash #=> "$2a$12$k/Ub1I2iomi84RacqY89Hu4.M0vK7klRnRtzorDyvOkVI.hKhkNw."
479
+
480
+ # when password hash is stored in a separate table
481
+ account.password_hash #=> #<Account::PasswordHash...> (record from `account_password_hashes` table)
482
+ account.password_hash.password_hash #=> "$2a$12$k/Ub1..." (inaccessible when using database authentication functions)
483
+
484
+ account.password = nil # clears password hash
485
+ account.password_hash #=> nil
486
+ ```
487
+
488
+ Note that the password attribute doesn't come with validations, making it
489
+ unsuitable for forms. It was primarily intended to allow easily creating
490
+ accounts in development console and in tests.
491
+
492
+ #### Associations
493
+
494
+ The `Rodauth::Rails::Model` mixin defines associations for Rodauth tables
495
+ associated to the accounts table:
496
+
497
+ ```rb
498
+ account.remember_key #=> #<Account::RememberKey> (record from `account_remember_keys` table)
499
+ account.active_session_keys #=> [#<Account::ActiveSessionKey>,...] (records from `account_active_session_keys` table)
500
+ ```
501
+
502
+ You can also reference the associated models directly:
503
+
504
+ ```rb
505
+ # model referencing the `account_authentication_audit_logs` table
506
+ Account::AuthenticationAuditLog.where(message: "login").group(:account_id)
507
+ ```
508
+
509
+ The associated models define the inverse `belongs_to :account` association:
510
+
511
+ ```rb
512
+ Account::ActiveSessionKey.includes(:account).map(&:account)
513
+ ```
514
+
515
+ Here is an example of using associations to create a method that returns
516
+ whether the account has multifactor authentication enabled:
517
+
518
+ ```rb
519
+ class Account < ApplicationRecord
520
+ include Rodauth::Rails.model
521
+
522
+ def mfa_enabled?
523
+ otp_key || (sms_code && sms_code.num_failures.nil?) || recovery_codes.any?
524
+ end
525
+ end
526
+ ```
527
+
528
+ Here is another example of creating a query scope that selects accounts with
529
+ multifactor authentication enabled:
530
+
531
+ ```rb
532
+ class Account < ApplicationRecord
533
+ include Rodauth::Rails.model
534
+
535
+ scope :otp_setup, -> { where(otp_key: OtpKey.all) }
536
+ scope :sms_codes_setup, -> { where(sms_code: SmsCode.where(num_failures: nil)) }
537
+ scope :recovery_codes_setup, -> { where(recovery_codes: RecoveryCode.all) }
538
+ scope :mfa_enabled, -> { merge(otp_setup.or(sms_codes_setup).or(recovery_codes_setup)) }
539
+ end
540
+ ```
541
+
542
+ Below is a list of all associations defined depending on the features loaded:
543
+
544
+ | Feature | Association | Type | Model | Table (default) |
545
+ | :------ | :---------- | :--- | :---- | :---- |
546
+ | account_expiration | `:activity_time` | `has_one` | `ActivityTime` | `account_activity_times` |
547
+ | active_sessions | `:active_session_keys` | `has_many` | `ActiveSessionKey` | `account_active_session_keys` |
548
+ | audit_logging | `:authentication_audit_logs` | `has_many` | `AuthenticationAuditLog` | `account_authentication_audit_logs` |
549
+ | disallow_password_reuse | `:previous_password_hashes` | `has_many` | `PreviousPasswordHash` | `account_previous_password_hashes` |
550
+ | email_auth | `:email_auth_key` | `has_one` | `EmailAuthKey` | `account_email_auth_keys` |
551
+ | jwt_refresh | `:jwt_refresh_keys` | `has_many` | `JwtRefreshKey` | `account_jwt_refresh_keys` |
552
+ | lockout | `:lockout` | `has_one` | `Lockout` | `account_lockouts` |
553
+ | lockout | `:login_failure` | `has_one` | `LoginFailure` | `account_login_failures` |
554
+ | otp | `:otp_key` | `has_one` | `OtpKey` | `account_otp_keys` |
555
+ | password_expiration | `:password_change_time` | `has_one` | `PasswordChangeTime` | `account_password_change_times` |
556
+ | recovery_codes | `:recovery_codes` | `has_many` | `RecoveryCode` | `account_recovery_codes` |
557
+ | remember | `:remember_key` | `has_one` | `RememberKey` | `account_remember_keys` |
558
+ | reset_password | `:password_reset_key` | `has_one` | `PasswordResetKey` | `account_password_reset_keys` |
559
+ | single_session | `:session_key` | `has_one` | `SessionKey` | `account_session_keys` |
560
+ | sms_codes | `:sms_code` | `has_one` | `SmsCode` | `account_sms_codes` |
561
+ | verify_account | `:verification_key` | `has_one` | `VerificationKey` | `account_verification_keys` |
562
+ | verify_login_change | `:login_change_key` | `has_one` | `LoginChangeKey` | `account_login_change_keys` |
563
+ | webauthn | `:webauthn_keys` | `has_many` | `WebauthnKey` | `account_webauthn_keys` |
564
+ | webauthn | `:webauthn_user_id` | `has_one` | `WebauthnUserId` | `account_webauthn_user_ids` |
565
+
566
+ By default, all associations except for audit logs have `dependent: :destroy`
567
+ set, to allow for easy deletion of account records in the console. You can use
568
+ `:association_options` to modify global or per-association options:
569
+
570
+ ```rb
571
+ # don't auto-delete associations when account model is deleted
572
+ Rodauth::Rails.model(association_options: { dependent: nil })
573
+
574
+ # require authentication audit logs to be eager loaded before retrieval
575
+ Rodauth::Rails.model(association_options: -> (name) {
576
+ { strict_loading: true } if name == :authentication_audit_logs
577
+ })
578
+ ```
579
+
456
580
  ### Multiple configurations
457
581
 
458
582
  If you need to handle multiple types of accounts that require different
@@ -656,37 +780,53 @@ class RodauthApp < Rodauth::Rails::App
656
780
  end
657
781
  ```
658
782
 
659
- ### Rodauth instance
783
+ ### Outside of a request
784
+
785
+ In some cases you might need to use Rodauth more programmatically. If you would
786
+ like to perform Rodauth operations outside of request context, Rodauth ships
787
+ with the [internal_request] feature just for that. The rodauth-rails gem
788
+ additionally updates the internal rack env hash with your
789
+ `config.action_mailer.default_url_options`, which is used for generating URLs.
660
790
 
661
- In some cases you might need to use Rodauth more programmatically, and perform
662
- Rodauth operations outside of the request context. rodauth-rails gives you a
663
- helper method for building a Rodauth instance:
791
+ If you need to access Rodauth methods not exposed as internal requests, you can
792
+ use `Rodauth::Rails.rodauth` to retrieve the Rodauth instance used by the
793
+ internal_request feature:
664
794
 
665
795
  ```rb
666
- rodauth = Rodauth::Rails.rodauth # or Rodauth::Rails.rodauth(:admin)
796
+ # app/lib/rodauth_app.rb
797
+ class RodauthApp < Rodauth::Rails::App
798
+ configure do
799
+ enable :internal_request # this is required
800
+ end
801
+ end
802
+ ```
803
+ ```rb
804
+ account = Account.find_by!(email: "user@example.com")
805
+ rodauth = Rodauth::Rails.rodauth(account: account)
667
806
 
668
- rodauth.login_url #=> "https://example.com/login"
669
- rodauth.account_from_login("user@example.com") # loads user by email
670
- rodauth.password_match?("secret") #=> true
671
- rodauth.setup_account_verification
672
- rodauth.close_account
807
+ rodauth.compute_hmac("token") #=> "TpEJTKfKwqYvIDKWsuZhkhKlhaBXtR1aodskBAflD8U"
808
+ rodauth.open_account? #=> true
809
+ rodauth.two_factor_authentication_setup? #=> true
810
+ rodauth.password_meets_requirements?("foo") #=> false
811
+ rodauth.locked_out? #=> false
673
812
  ```
674
813
 
675
- The base URL is taken from Action Mailer's `default_url_options` setting if
676
- configured. The `Rodauth::Rails.rodauth` method accepts additional keyword
677
- arguments:
814
+ In addition to the `:account` option, the `Rodauth::Rails.rodauth`
815
+ method accepts any options supported by the internal_request feature.
816
+
817
+ ```rb
818
+ Rodauth::Rails.rodauth(
819
+ env: { "HTTP_USER_AGENT" => "programmatic" },
820
+ session: { two_factor_auth_setup: true },
821
+ params: { "param" => "value" }
822
+ )
823
+ ```
678
824
 
679
- * `:account` Active Record model instance from which to set `account` and `session[:account_id]`
680
- * `:query` & `:form` – set specific query/form parameters
681
- * `:session` – set any session values
682
- * `:env` – set any additional Rack env values
825
+ Secondary Rodauth configurations are specified by passing the configuration
826
+ name:
683
827
 
684
828
  ```rb
685
- Rodauth::Rails.rodauth(account: Account.find(account_id))
686
- Rodauth::Rails.rodauth(query: { "param" => "value" })
687
- Rodauth::Rails.rodauth(form: { "param" => "value" })
688
- Rodauth::Rails.rodauth(session: { two_factor_auth_setup: true })
689
- Rodauth::Rails.rodauth(env: { "HTTP_USER_AGENT" => "programmatic" })
829
+ Rodauth::Rails.rodauth(:admin)
690
830
  ```
691
831
 
692
832
  ## How it works
@@ -825,21 +965,23 @@ class RodauthApp < Rodauth::Rails::App
825
965
  end
826
966
  ```
827
967
 
828
- If you need Cross-Origin Resource Sharing and/or JWT refresh tokens, enable the
829
- corresponding Rodauth features and create the necessary tables:
968
+ The JWT token will be returned after each request to Rodauth routes. To also
969
+ return the JWT token on requests to your app's routes, you can add the
970
+ following code to your base controller:
830
971
 
831
- ```sh
832
- $ rails generate rodauth:migration jwt_refresh
833
- $ rails db:migrate
834
- ```
835
972
  ```rb
836
- # app/lib/rodauth_app.rb
837
- class RodauthApp < Rodauth::Rails::App
838
- configure do
839
- # ...
840
- enable :jwt, :jwt_cors, :jwt_refresh
841
- # ...
973
+ class ApplicationController < ActionController::Base
974
+ # ...
975
+ after_action :set_jwt_token
976
+
977
+ private
978
+
979
+ def set_jwt_token
980
+ if rodauth.use_jwt? && rodauth.valid_jwt?
981
+ response.headers["Authorization"] = rodauth.session_jwt
982
+ end
842
983
  end
984
+ # ...
843
985
  end
844
986
  ```
845
987
 
@@ -1147,29 +1289,6 @@ Rails.application.configure do |config|
1147
1289
  end
1148
1290
  ```
1149
1291
 
1150
- If you need to create an account record with a password directly, you can do it
1151
- as follows:
1152
-
1153
- ```rb
1154
- # app/models/account.rb
1155
- class Account < ApplicationRecord
1156
- has_one :password_hash, foreign_key: :id
1157
- end
1158
- ```
1159
- ```rb
1160
- # app/models/account/password_hash.rb
1161
- class Account::PasswordHash < ApplicationRecord
1162
- belongs_to :account, foreign_key: :id
1163
- end
1164
- ```
1165
- ```rb
1166
- require "bcrypt"
1167
-
1168
- account = Account.create!(email: "user@example.com", status: "verified")
1169
- password_hash = BCrypt::Password.create("secret", cost: BCrypt::Engine::MIN_COST)
1170
- account.create_password_hash!(id: account.id, password_hash: password_hash)
1171
- ```
1172
-
1173
1292
  ## Rodauth defaults
1174
1293
 
1175
1294
  rodauth-rails changes some of the default Rodauth settings for easier setup:
@@ -1303,3 +1422,4 @@ conduct](https://github.com/janko/rodauth-rails/blob/master/CODE_OF_CONDUCT.md).
1303
1422
  [single_session]: http://rodauth.jeremyevans.net/rdoc/files/doc/single_session_rdoc.html
1304
1423
  [account_expiration]: http://rodauth.jeremyevans.net/rdoc/files/doc/account_expiration_rdoc.html
1305
1424
  [simple_ldap_authenticator]: https://github.com/jeremyevans/simple_ldap_authenticator
1425
+ [internal_request]: http://rodauth.jeremyevans.net/rdoc/files/doc/internal_request_rdoc.html
@@ -154,7 +154,7 @@ class RodauthApp < Rodauth::Rails::App
154
154
  <% end -%>
155
155
  end
156
156
 
157
- # ==> Multiple configurations
157
+ # ==> Secondary configurations
158
158
  # configure(:admin) do
159
159
  # # ... enable features ...
160
160
  # prefix "/admin"
@@ -163,11 +163,6 @@ class RodauthApp < Rodauth::Rails::App
163
163
  #
164
164
  # # search views in `app/views/admin/rodauth` directory
165
165
  # rails_controller { Admin::RodauthController }
166
- #
167
- # # use separate tables (requires creating the new tables)
168
- # methods.grep(/_table$/) do |table_method|
169
- # public_send(table_method) { :"admin_#{super()}" }
170
- # end
171
166
  # end
172
167
 
173
168
  route do |r|
@@ -189,7 +184,7 @@ class RodauthApp < Rodauth::Rails::App
189
184
  # rodauth.require_authentication
190
185
  # end
191
186
 
192
- # ==> Multiple configurations
187
+ # ==> Secondary configurations
193
188
  # r.on "admin" do
194
189
  # r.rodauth(:admin)
195
190
  #
@@ -197,7 +192,7 @@ class RodauthApp < Rodauth::Rails::App
197
192
  # rodauth(:admin).require_http_basic_auth
198
193
  # end
199
194
  #
200
- # r.pass # allow the Rails app to handle other "/admin/*" requests
195
+ # break # allow the Rails app to handle other "/admin/*" requests
201
196
  # end
202
197
  end
203
198
  end
@@ -1,2 +1,3 @@
1
1
  class Account < ApplicationRecord
2
+ include Rodauth::Rails.model
2
3
  end
@@ -129,8 +129,9 @@ module Rodauth
129
129
  end
130
130
 
131
131
  def controller
132
- rodauth = Rodauth::Rails.rodauth(configuration_name)
133
- rodauth.rails_controller
132
+ rodauth = Rodauth::Rails.app.rodauth(configuration_name)
133
+ fail ArgumentError, "unknown rodauth configuration: #{configuration_name.inspect}" unless rodauth
134
+ rodauth.allocate.rails_controller
134
135
  end
135
136
 
136
137
  def configuration_name
data/lib/rodauth/rails.rb CHANGED
@@ -1,9 +1,6 @@
1
1
  require "rodauth/rails/version"
2
2
  require "rodauth/rails/railtie"
3
3
 
4
- require "rack/utils"
5
- require "stringio"
6
-
7
4
  module Rodauth
8
5
  module Rails
9
6
  class Error < StandardError
@@ -12,49 +9,49 @@ module Rodauth
12
9
  # This allows the developer to avoid loading Rodauth at boot time.
13
10
  autoload :App, "rodauth/rails/app"
14
11
  autoload :Auth, "rodauth/rails/auth"
12
+ autoload :Model, "rodauth/rails/model"
15
13
 
16
14
  @app = nil
17
15
  @middleware = true
18
16
 
17
+ LOCK = Mutex.new
18
+
19
19
  class << self
20
- def rodauth(name = nil, query: {}, form: {}, session: {}, account: nil, env: {})
21
- unless app.rodauth(name)
20
+ def rodauth(name = nil, query: nil, form: nil, account: nil, **options)
21
+ auth_class = app.rodauth(name)
22
+
23
+ unless auth_class
22
24
  fail ArgumentError, "undefined rodauth configuration: #{name.inspect}"
23
25
  end
24
26
 
25
- url_options = ActionMailer::Base.default_url_options
26
-
27
- scheme = url_options[:protocol] || "http"
28
- port = url_options[:port]
29
- port ||= Rack::Request::DEFAULT_PORTS[scheme] if Gem::Version.new(Rack.release) < Gem::Version.new("2.0")
30
- host = url_options[:host]
31
- host += ":#{port}" if port
32
-
33
- content_type = "application/x-www-form-urlencoded" if form.any?
34
-
35
- rack_env = {
36
- "QUERY_STRING" => Rack::Utils.build_nested_query(query),
37
- "rack.input" => StringIO.new(Rack::Utils.build_nested_query(form)),
38
- "CONTENT_TYPE" => content_type,
39
- "rack.session" => {},
40
- "HTTP_HOST" => host,
41
- "rack.url_scheme" => scheme,
42
- }.merge(env)
43
-
44
- scope = app.new(rack_env)
45
- instance = scope.rodauth(name)
27
+ LOCK.synchronize do
28
+ unless auth_class.features.include?(:internal_request)
29
+ auth_class.configure { enable :internal_request }
30
+ warn "Rodauth::Rails.rodauth requires the internal_request feature to be enabled. For now it was enabled automatically, but this behaviour will be removed in version 1.0."
31
+ end
32
+ end
46
33
 
47
- # update session hash here to make it work with JWT session
48
- instance.session.merge!(session)
34
+ if query || form
35
+ warn "The :query and :form keyword arguments for Rodauth::Rails.rodauth have been deprecated. Please use the :params argument supported by internal_request feature instead."
36
+ options[:params] = query || form
37
+ end
49
38
 
50
39
  if account
51
- instance.instance_variable_set(:@account, account.attributes.symbolize_keys)
52
- instance.session[instance.session_key] = instance.account_session_value
40
+ options[:account_id] = account.id
41
+ end
42
+
43
+ instance = auth_class.internal_request_eval(options) do
44
+ @account = account.attributes.symbolize_keys if account
45
+ self
53
46
  end
54
47
 
55
48
  instance
56
49
  end
57
50
 
51
+ def model(name = nil, **options)
52
+ Rodauth::Rails::Model.new(app.rodauth(name), **options)
53
+ end
54
+
58
55
  # routing constraint that requires authentication
59
56
  def authenticated(name = nil, &condition)
60
57
  lambda do |request|
@@ -10,6 +10,7 @@ module Rodauth
10
10
  require "rodauth/rails/feature/render"
11
11
  require "rodauth/rails/feature/email"
12
12
  require "rodauth/rails/feature/instrumentation"
13
+ require "rodauth/rails/feature/internal_request"
13
14
 
14
15
  include Rodauth::Rails::Feature::Base
15
16
  include Rodauth::Rails::Feature::Callbacks
@@ -17,5 +18,6 @@ module Rodauth
17
18
  include Rodauth::Rails::Feature::Render
18
19
  include Rodauth::Rails::Feature::Email
19
20
  include Rodauth::Rails::Feature::Instrumentation
21
+ include Rodauth::Rails::Feature::InternalRequest
20
22
  end
21
23
  end
@@ -4,13 +4,17 @@ module Rodauth
4
4
  module Callbacks
5
5
  private
6
6
 
7
+ def _around_rodauth
8
+ rails_controller_around { super }
9
+ end
10
+
7
11
  # Runs controller callbacks and rescue handlers around Rodauth actions.
8
- def _around_rodauth(&block)
12
+ def rails_controller_around
9
13
  result = nil
10
14
 
11
15
  rails_controller_rescue do
12
16
  rails_controller_callbacks do
13
- result = catch(:halt) { super(&block) }
17
+ result = catch(:halt) { yield }
14
18
  end
15
19
  end
16
20
 
@@ -0,0 +1,50 @@
1
+ module Rodauth
2
+ module Rails
3
+ module Feature
4
+ module InternalRequest
5
+ def post_configure
6
+ super
7
+ return unless internal_request?
8
+
9
+ self.class.define_singleton_method(:internal_request) do |route, opts = {}, &blk|
10
+ url_options = ::Rails.application.config.action_mailer.default_url_options || {}
11
+
12
+ scheme = url_options[:protocol]
13
+ port = url_options[:port]
14
+ port||= Rack::Request::DEFAULT_PORTS[scheme] if Rack.release < "2"
15
+ host = url_options[:host]
16
+ host_with_port = host && port ? "#{host}:#{port}" : host
17
+
18
+ env = {
19
+ "HTTP_HOST" => host_with_port,
20
+ "rack.url_scheme" => scheme,
21
+ "SERVER_NAME" => host,
22
+ "SERVER_PORT" => port,
23
+ }.compact
24
+
25
+ opts = opts.merge(env: env) { |k, v1, v2| v2.merge(v1) }
26
+
27
+ super(route, opts, &blk)
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def rails_controller_around
34
+ return yield if internal_request?
35
+ super
36
+ end
37
+
38
+ def rails_instrument_request
39
+ return yield if internal_request?
40
+ super
41
+ end
42
+
43
+ def rails_instrument_redirection
44
+ return yield if internal_request?
45
+ super
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,101 @@
1
+ module Rodauth
2
+ module Rails
3
+ class Model < Module
4
+ require "rodauth/rails/model/associations"
5
+
6
+ def initialize(auth_class, association_options: {})
7
+ @auth_class = auth_class
8
+ @association_options = association_options
9
+
10
+ define_methods
11
+ end
12
+
13
+ def included(model)
14
+ fail Rodauth::Rails::Error, "must be an Active Record model" unless model < ActiveRecord::Base
15
+
16
+ define_associations(model)
17
+ end
18
+
19
+ private
20
+
21
+ def define_methods
22
+ rodauth = @auth_class.allocate.freeze
23
+
24
+ attr_reader :password
25
+
26
+ define_method(:password=) do |password|
27
+ @password = password
28
+ password_hash = rodauth.send(:password_hash, password) if password
29
+ set_password_hash(password_hash)
30
+ end
31
+
32
+ define_method(:set_password_hash) do |password_hash|
33
+ if rodauth.account_password_hash_column
34
+ public_send(:"#{rodauth.account_password_hash_column}=", password_hash)
35
+ else
36
+ if password_hash
37
+ record = self.password_hash || build_password_hash
38
+ record.public_send(:"#{rodauth.password_hash_column}=", password_hash)
39
+ else
40
+ self.password_hash&.mark_for_destruction
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ def define_associations(model)
47
+ define_password_hash_association(model) unless rodauth.account_password_hash_column
48
+
49
+ feature_associations.each do |association|
50
+ define_association(model, **association)
51
+ end
52
+ end
53
+
54
+ def define_password_hash_association(model)
55
+ password_hash_id_column = rodauth.password_hash_id_column
56
+ scope = -> { select(password_hash_id_column) } if rodauth.send(:use_database_authentication_functions?)
57
+
58
+ define_association model,
59
+ type: :has_one,
60
+ name: :password_hash,
61
+ table: rodauth.password_hash_table,
62
+ foreign_key: password_hash_id_column,
63
+ scope: scope,
64
+ autosave: true
65
+ end
66
+
67
+ def define_association(model, type:, name:, table:, foreign_key:, scope: nil, **options)
68
+ associated_model = Class.new(model.superclass)
69
+ associated_model.table_name = table
70
+ associated_model.belongs_to :account,
71
+ class_name: model.name,
72
+ foreign_key: foreign_key,
73
+ inverse_of: name
74
+
75
+ model.const_set(name.to_s.singularize.camelize, associated_model)
76
+
77
+ model.public_send type, name, scope,
78
+ class_name: associated_model.name,
79
+ foreign_key: foreign_key,
80
+ dependent: :destroy,
81
+ inverse_of: :account,
82
+ **options,
83
+ **association_options(name)
84
+ end
85
+
86
+ def feature_associations
87
+ Rodauth::Rails::Model::Associations.call(rodauth)
88
+ end
89
+
90
+ def association_options(name)
91
+ options = @association_options
92
+ options = options.call(name) if options.respond_to?(:call)
93
+ options || {}
94
+ end
95
+
96
+ def rodauth
97
+ @auth_class.allocate
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,195 @@
1
+ module Rodauth
2
+ module Rails
3
+ class Model
4
+ class Associations
5
+ attr_reader :rodauth
6
+
7
+ def self.call(rodauth)
8
+ new(rodauth).call
9
+ end
10
+
11
+ def initialize(rodauth)
12
+ @rodauth = rodauth
13
+ end
14
+
15
+ def call
16
+ rodauth.features
17
+ .select { |feature| respond_to?(feature, true) }
18
+ .flat_map { |feature| send(feature) }
19
+ end
20
+
21
+ private
22
+
23
+ def remember
24
+ {
25
+ name: :remember_key,
26
+ type: :has_one,
27
+ table: rodauth.remember_table,
28
+ foreign_key: rodauth.remember_id_column,
29
+ }
30
+ end
31
+
32
+ def verify_account
33
+ {
34
+ name: :verification_key,
35
+ type: :has_one,
36
+ table: rodauth.verify_account_table,
37
+ foreign_key: rodauth.verify_account_id_column,
38
+ }
39
+ end
40
+
41
+ def reset_password
42
+ {
43
+ name: :password_reset_key,
44
+ type: :has_one,
45
+ table: rodauth.reset_password_table,
46
+ foreign_key: rodauth.reset_password_id_column,
47
+ }
48
+ end
49
+
50
+ def verify_login_change
51
+ {
52
+ name: :login_change_key,
53
+ type: :has_one,
54
+ table: rodauth.verify_login_change_table,
55
+ foreign_key: rodauth.verify_login_change_id_column,
56
+ }
57
+ end
58
+
59
+ def lockout
60
+ [
61
+ {
62
+ name: :lockout,
63
+ type: :has_one,
64
+ table: rodauth.account_lockouts_table,
65
+ foreign_key: rodauth.account_lockouts_id_column,
66
+ },
67
+ {
68
+ name: :login_failure,
69
+ type: :has_one,
70
+ table: rodauth.account_login_failures_table,
71
+ foreign_key: rodauth.account_login_failures_id_column,
72
+ }
73
+ ]
74
+ end
75
+
76
+ def email_auth
77
+ {
78
+ name: :email_auth_key,
79
+ type: :has_one,
80
+ table: rodauth.email_auth_table,
81
+ foreign_key: rodauth.email_auth_id_column,
82
+ }
83
+ end
84
+
85
+ def account_expiration
86
+ {
87
+ name: :activity_time,
88
+ type: :has_one,
89
+ table: rodauth.account_activity_table,
90
+ foreign_key: rodauth.account_activity_id_column,
91
+ }
92
+ end
93
+
94
+ def active_sessions
95
+ {
96
+ name: :active_session_keys,
97
+ type: :has_many,
98
+ table: rodauth.active_sessions_table,
99
+ foreign_key: rodauth.active_sessions_account_id_column,
100
+ }
101
+ end
102
+
103
+ def audit_logging
104
+ {
105
+ name: :authentication_audit_logs,
106
+ type: :has_many,
107
+ table: rodauth.audit_logging_table,
108
+ foreign_key: rodauth.audit_logging_account_id_column,
109
+ dependent: nil,
110
+ }
111
+ end
112
+
113
+ def disallow_password_reuse
114
+ {
115
+ name: :previous_password_hashes,
116
+ type: :has_many,
117
+ table: rodauth.previous_password_hash_table,
118
+ foreign_key: rodauth.previous_password_account_id_column,
119
+ }
120
+ end
121
+
122
+ def jwt_refresh
123
+ {
124
+ name: :jwt_refresh_keys,
125
+ type: :has_many,
126
+ table: rodauth.jwt_refresh_token_table,
127
+ foreign_key: rodauth.jwt_refresh_token_account_id_column,
128
+ }
129
+ end
130
+
131
+ def password_expiration
132
+ {
133
+ name: :password_change_time,
134
+ type: :has_one,
135
+ table: rodauth.password_expiration_table,
136
+ foreign_key: rodauth.password_expiration_id_column,
137
+ }
138
+ end
139
+
140
+ def single_session
141
+ {
142
+ name: :session_key,
143
+ type: :has_one,
144
+ table: rodauth.single_session_table,
145
+ foreign_key: rodauth.single_session_id_column,
146
+ }
147
+ end
148
+
149
+ def otp
150
+ {
151
+ name: :otp_key,
152
+ type: :has_one,
153
+ table: rodauth.otp_keys_table,
154
+ foreign_key: rodauth.otp_keys_id_column,
155
+ }
156
+ end
157
+
158
+ def sms_codes
159
+ {
160
+ name: :sms_code,
161
+ type: :has_one,
162
+ table: rodauth.sms_codes_table,
163
+ foreign_key: rodauth.sms_id_column,
164
+ }
165
+ end
166
+
167
+ def recovery_codes
168
+ {
169
+ name: :recovery_codes,
170
+ type: :has_many,
171
+ table: rodauth.recovery_codes_table,
172
+ foreign_key: rodauth.recovery_codes_id_column,
173
+ }
174
+ end
175
+
176
+ def webauthn
177
+ [
178
+ {
179
+ name: :webauthn_user_id,
180
+ type: :has_one,
181
+ table: rodauth.webauthn_user_ids_table,
182
+ foreign_key: rodauth.webauthn_user_ids_account_id_column,
183
+ },
184
+ {
185
+ name: :webauthn_keys,
186
+ type: :has_many,
187
+ table: rodauth.webauthn_keys_table,
188
+ foreign_key: rodauth.webauthn_keys_account_id_column,
189
+ }
190
+ ]
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
@@ -4,15 +4,15 @@ namespace :rodauth do
4
4
 
5
5
  puts "Routes handled by #{app}:"
6
6
 
7
- app.opts[:rodauths].each_key do |rodauth_name|
8
- rodauth = Rodauth::Rails.rodauth(rodauth_name)
7
+ app.opts[:rodauths].each do |configuration_name, auth_class|
8
+ auth_class.configure { enable :path_class_methods }
9
9
 
10
- routes = rodauth.class.routes.map do |handle_method|
10
+ routes = auth_class.routes.map do |handle_method|
11
11
  path_method = "#{handle_method.to_s.sub(/\Ahandle_/, "")}_path"
12
12
 
13
13
  [
14
- rodauth.public_send(path_method),
15
- "rodauth#{rodauth_name && "(:#{rodauth_name})"}.#{path_method}",
14
+ auth_class.public_send(path_method),
15
+ "rodauth#{configuration_name && "(:#{configuration_name})"}.#{path_method}",
16
16
  ]
17
17
  end
18
18
 
@@ -1,5 +1,5 @@
1
1
  module Rodauth
2
2
  module Rails
3
- VERSION = "0.14.0"
3
+ VERSION = "0.15.0"
4
4
  end
5
5
  end
@@ -17,10 +17,13 @@ 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.11"
20
+ spec.add_dependency "rodauth", "~> 2.15"
21
21
  spec.add_dependency "sequel-activerecord_connection", "~> 1.1"
22
22
  spec.add_dependency "tilt"
23
23
  spec.add_dependency "bcrypt"
24
24
 
25
25
  spec.add_development_dependency "jwt"
26
+ spec.add_development_dependency "rotp"
27
+ spec.add_development_dependency "rqrcode"
28
+ spec.add_development_dependency "webauthn" unless RUBY_ENGINE == "jruby"
26
29
  end
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.14.0
4
+ version: 0.15.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: 2021-07-10 00:00:00.000000000 Z
11
+ date: 2021-07-29 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.11'
39
+ version: '2.15'
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.11'
46
+ version: '2.15'
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: sequel-activerecord_connection
49
49
  requirement: !ruby/object:Gem::Requirement
@@ -100,6 +100,48 @@ dependencies:
100
100
  - - ">="
101
101
  - !ruby/object:Gem::Version
102
102
  version: '0'
103
+ - !ruby/object:Gem::Dependency
104
+ name: rotp
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ - !ruby/object:Gem::Dependency
118
+ name: rqrcode
119
+ requirement: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ type: :development
125
+ prerelease: false
126
+ version_requirements: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ - !ruby/object:Gem::Dependency
132
+ name: webauthn
133
+ requirement: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ type: :development
139
+ prerelease: false
140
+ version_requirements: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
103
145
  description: Provides Rails integration for Rodauth.
104
146
  email:
105
147
  - janko.marohnic@gmail.com
@@ -212,8 +254,11 @@ files:
212
254
  - lib/rodauth/rails/feature/csrf.rb
213
255
  - lib/rodauth/rails/feature/email.rb
214
256
  - lib/rodauth/rails/feature/instrumentation.rb
257
+ - lib/rodauth/rails/feature/internal_request.rb
215
258
  - lib/rodauth/rails/feature/render.rb
216
259
  - lib/rodauth/rails/middleware.rb
260
+ - lib/rodauth/rails/model.rb
261
+ - lib/rodauth/rails/model/associations.rb
217
262
  - lib/rodauth/rails/railtie.rb
218
263
  - lib/rodauth/rails/tasks.rake
219
264
  - lib/rodauth/rails/version.rb