rodauth-rails 0.9.1 → 0.10.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: b8f8aec1dbdc745a530aabec0d63bc2681499dd36f8185faed9ea09e7184636e
4
- data.tar.gz: fbc5a75976a922978a6e37fee3bef8e7f04bb0a9a324066afdf79172b33f00e9
3
+ metadata.gz: 4cc138f57505a1bbf92267e02b8d9b87f50c0dd9b6c114891f649c0b15878637
4
+ data.tar.gz: 28fc8264a8629dd186a446a4d067167d572f8ea58b65c742f12f81a5192221db
5
5
  SHA512:
6
- metadata.gz: 89d2f6ad377ba8e3f18bc747c3bfdf53e97c1a29f2731036987e5f7c1fde14db89732cda2d09026a153d81eabe26e51e021a129f02517d4d5582fcaf392876ca
7
- data.tar.gz: 648b1297a9569b436113b5921a9ae37944d808ed42a03ef57a75452a74143dcc493e7d9c34a12f31f780745db5d2b1365d5a7b602dfa303571961730566852f4
6
+ metadata.gz: 74645990b10677d44503f272a63300465881c6e596b514ce0e1d1607689d8ace60c5e70a79c952256ad73bf704a8d268e27af3da8cdb617c5b381f752b302c4b
7
+ data.tar.gz: 1525c4a51d4323e348ee2dc117e5ef320214f42f385549f5646af1d8e1792bfeac2be3f220b473873aed8fc72f4448aade9848b82fdd2c348874f8b4950e4631
data/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## 0.10.0 (2021-03-23)
2
+
3
+ * Add `Rodauth::Rails::Auth` superclass for moving configurations into separate files (@janko)
4
+
5
+ * Load the `pass` Roda plugin and recommend calling `r.pass` on prefixed routes (@janko)
6
+
7
+ * Improve Roda middleware inspect output (@janko)
8
+
9
+ * Create `RodauthMailer` and email templates in `rodauth:install`, and remove `rodauth:mailer` (@janko)
10
+
11
+ * Raise `KeyError` in `#rodauth` method when the Rodauth instance doesn't exist (@janko)
12
+
13
+ * Add `Rodauth::Rails.authenticated` routing constraint for requiring authentication (@janko)
14
+
1
15
  ## 0.9.1 (2021-02-10)
2
16
 
3
17
  * Fix flash integration being loaded for API-only apps and causing an error (@dmitryzuev)
data/README.md CHANGED
@@ -83,7 +83,7 @@ documentation][hmac] for instructions on how to safely transition, or just set
83
83
  Add the gem to your Gemfile:
84
84
 
85
85
  ```rb
86
- gem "rodauth-rails", "~> 0.9"
86
+ gem "rodauth-rails", "~> 0.10"
87
87
 
88
88
  # gem "jwt", require: false # for JWT feature
89
89
  # gem "rotp", require: false # for OTP feature
@@ -116,6 +116,7 @@ The generator will create the following files:
116
116
  * Rodauth app at `app/lib/rodauth_app.rb`
117
117
  * Rodauth controller at `app/controllers/rodauth_controller.rb`
118
118
  * Account model at `app/models/account.rb`
119
+ * Rodauth mailer at `app/mailers/rodauth_mailer.rb` with views
119
120
 
120
121
  ### Migration
121
122
 
@@ -211,6 +212,23 @@ class Account < ApplicationRecord
211
212
  end
212
213
  ```
213
214
 
215
+ ### Rodauth mailer
216
+
217
+ The default Rodauth app is configured to use `RodauthMailer` mailer
218
+ for sending authentication emails.
219
+
220
+ ```rb
221
+ # app/mailers/rodauth_mailer.rb
222
+ class RodauthMailer < ApplicationMailer
223
+ def verify_account(recipient, email_link) ... end
224
+ def reset_password(recipient, email_link) ... end
225
+ def verify_login_change(recipient, old_login, new_login, email_link) ... end
226
+ def password_changed(recipient) ... end
227
+ # def email_auth(recipient, email_link) ... end
228
+ # def unlock_account(recipient, email_link) ... end
229
+ end
230
+ ```
231
+
214
232
  ## Usage
215
233
 
216
234
  ### Routes
@@ -272,7 +290,7 @@ class ApplicationController < ActionController::Base
272
290
  rodauth.logout
273
291
  rodauth.login_required
274
292
  end
275
- helper_method :current_account
293
+ helper_method :current_account # skip if inheriting from ActionController:API
276
294
  end
277
295
  ```
278
296
 
@@ -330,15 +348,52 @@ class PostsController < ApplicationController
330
348
  end
331
349
  ```
332
350
 
333
- Or at the Rails router level:
351
+ #### Routing constraints
352
+
353
+ You can also require authentication at the Rails router level by
354
+ using a built-in `authenticated` routing constraint:
334
355
 
335
356
  ```rb
336
357
  # config/routes.rb
337
358
  Rails.application.routes.draw do
338
- constraints -> (r) { r.env["rodauth"].require_authentication } do
339
- namespace :admin do
340
- # ...
341
- end
359
+ constraints Rodauth::Rails.authenticated do
360
+ # ... authenticated routes ...
361
+ end
362
+ end
363
+ ```
364
+
365
+ If you want additional conditions, you can pass in a block, which is
366
+ called with the Rodauth instance:
367
+
368
+ ```rb
369
+ # config/routes.rb
370
+ Rails.application.routes.draw do
371
+ # require multifactor authentication to be setup
372
+ constraints Rodauth::Rails.authenticated { |rodauth| rodauth.uses_two_factor_authentication? } do
373
+ # ...
374
+ end
375
+ end
376
+ ```
377
+
378
+ You can specify the Rodauth configuration by passing the configuration name:
379
+
380
+ ```rb
381
+ # config/routes.rb
382
+ Rails.application.routes.draw do
383
+ constraints Rodauth::Rails.authenticated(:admin) do
384
+ # ...
385
+ end
386
+ end
387
+ ```
388
+
389
+ If you need something more custom, you can always create the routing constraint
390
+ manually:
391
+
392
+ ```rb
393
+ # config/routes.rb
394
+ Rails.application.routes.draw do
395
+ constraints -> (r) { !r.env["rodauth"].logged_in? } do # or "rodauth.admin"
396
+ # routes when the user is not logged in
342
397
  end
343
398
  end
344
399
  ```
@@ -406,54 +461,33 @@ end
406
461
 
407
462
  ### Mailer
408
463
 
409
- Depending on the features you've enabled, Rodauth may send emails as part of
410
- the authentication flow. Most email settings can be customized:
464
+ The install generator will create `RodauthMailer` with default email templates,
465
+ and configure Rodauth features that send emails as part of the authentication
466
+ flow to use it.
411
467
 
412
468
  ```rb
413
- # app/lib/rodauth_app.rb
414
- class RodauthApp < Rodauth::Rails::App
415
- # ...
416
- configure do
469
+ # app/mailers/rodauth_mailer.rb
470
+ class RodauthMailer < ApplicationMailer
471
+ def verify_account(recipient, email_link)
417
472
  # ...
418
- # general settings
419
- email_from "no-reply@myapp.com"
420
- email_subject_prefix "[MyApp] "
421
- send_email(&:deliver_later)
473
+ end
474
+ def reset_password(recipient, email_link)
422
475
  # ...
423
- # feature settings
424
- verify_account_email_subject "Verify your account"
425
- verify_account_email_body { "Verify your account by visting this link: #{verify_account_email_link}" }
476
+ end
477
+ def verify_login_change(recipient, old_login, new_login, email_link)
426
478
  # ...
427
479
  end
480
+ def password_changed(recipient)
481
+ # ...
482
+ end
483
+ # def email_auth(recipient, email_link)
484
+ # ...
485
+ # end
486
+ # def unlock_account(recipient, email_link)
487
+ # ...
488
+ # end
428
489
  end
429
490
  ```
430
-
431
- This is convenient when starting out, but eventually you might want to use your
432
- own mailer. You can start by running the following command:
433
-
434
- ```sh
435
- $ rails generate rodauth:mailer
436
- ```
437
-
438
- This will create a `RodauthMailer` with the associated mailer views in
439
- `app/views/rodauth_mailer` directory:
440
-
441
- ```rb
442
- # app/mailers/rodauth_mailer.rb
443
- class RodauthMailer < ApplicationMailer
444
- def verify_account(recipient, email_link) ... end
445
- def reset_password(recipient, email_link) ... end
446
- def verify_login_change(recipient, old_login, new_login, email_link) ... end
447
- def password_changed(recipient) ... end
448
- # def email_auth(recipient, email_link) ... end
449
- # def unlock_account(recipient, email_link) ... end
450
- end
451
- ```
452
-
453
- You can then uncomment the lines in your Rodauth configuration to have it call
454
- your mailer. If you've enabled additional authentication features that send
455
- emails, make sure to override their `create_*_email` methods as well.
456
-
457
491
  ```rb
458
492
  # app/lib/rodauth_app.rb
459
493
  class RodauthApp < Rodauth::Rails::App
@@ -487,10 +521,17 @@ class RodauthApp < Rodauth::Rails::App
487
521
  end
488
522
  ```
489
523
 
490
- This approach can be used even if you're using a 3rd-party service for
491
- transactional emails, where emails are sent via HTTP instead of SMTP. Whatever
492
- the `create_*_email` block returns will be passed to `send_email`, so you can
493
- be creative.
524
+ The above configuration uses `#deliver_later`, which assumes Active Job is
525
+ configured. It's generally recommended to send emails in a background job,
526
+ for better throughput and ability to retry. However, if you want to send emails
527
+ synchronously, you can modify the code to call `#deliver` instead.
528
+
529
+ The `#send_email` method will receive whatever object is returned by the
530
+ `#create_*_email` methods. But if that doesn't suit you, you can override
531
+ `#send_*_email` methods instead, which are expected to send the email
532
+ immediately. This might work better in scenarios such as using a 3rd-party
533
+ service for transactional emails, where emails are sent via HTTP instead of
534
+ SMTP.
494
535
 
495
536
  ### Migrations
496
537
 
@@ -531,12 +572,20 @@ class RodauthApp < Rodauth::Rails::App
531
572
  prefix "/admin"
532
573
  session_key_prefix "admin_"
533
574
  remember_cookie_key "_admin_remember" # if using remember feature
575
+
576
+ # if you want separate tables
577
+ accounts_table :admin_accounts
578
+ password_hash_table :admin_account_password_hashes
534
579
  # ...
535
580
  end
536
581
 
537
582
  route do |r|
538
583
  r.rodauth
539
- r.on("admin") { r.rodauth(:admin) }
584
+
585
+ r.on "admin" do
586
+ r.rodauth(:admin)
587
+ r.pass # allow the Rails app to handle other "/admin/*" requests
588
+ end
540
589
  # ...
541
590
  end
542
591
  end
@@ -548,6 +597,98 @@ Then in your application you can reference the secondary Rodauth instance:
548
597
  rodauth(:admin).login_path #=> "/admin/login"
549
598
  ```
550
599
 
600
+ #### Named auth classes
601
+
602
+ A `configure` block inside `Rodauth::Rails::App` will internally create an
603
+ anonymous `Rodauth::Auth` subclass, and register it under the given name.
604
+ However, you can also define the auth classes explicitly, by creating
605
+ subclasses of `Rodauth::Rails::Auth`:
606
+
607
+ ```rb
608
+ # app/lib/rodauth_main.rb
609
+ class RodauthMain < Rodauth::Rails::Auth
610
+ configure do
611
+ # ... main configuration ...
612
+ end
613
+ end
614
+ ```
615
+ ```rb
616
+ # app/lib/rodauth_admin.rb
617
+ class RodauthAdmin < Rodauth::Rails::Auth
618
+ configure do
619
+ # ...
620
+ prefix "/admin"
621
+ session_key_prefix "admin_"
622
+ # ...
623
+ end
624
+ end
625
+ ```
626
+ ```rb
627
+ # app/lib/rodauth_app.rb
628
+ class RodauthApp < Rodauth::Rails::App
629
+ configure RodauthMain
630
+ configure RodauthAdmin, :admin
631
+ # ...
632
+ end
633
+ ```
634
+
635
+ This allows having each configuration in a dedicated file, and named constants
636
+ improve introspection and error messages. You can also use inheritance to share
637
+ common settings:
638
+
639
+ ```rb
640
+ # app/lib/rodauth_base.rb
641
+ class RodauthBase < Rodauth::Rails::App
642
+ # common settings that can be shared between multiple configurations
643
+ configure do
644
+ enable :login, :logout
645
+ login_return_to_requested_location? true
646
+ logout_redirect "/"
647
+ # ...
648
+ end
649
+ end
650
+ ```
651
+ ```rb
652
+ # app/lib/rodauth_main.rb
653
+ class RodauthMain < RodauthBase # inherit common settings
654
+ configure do
655
+ # ... customize main ...
656
+ end
657
+ end
658
+ ```
659
+ ```rb
660
+ # app/lib/rodauth_admin.rb
661
+ class RodauthAdmin < RodauthBase # inherit common settings
662
+ configure do
663
+ # ... customize admin ...
664
+ end
665
+ end
666
+ ```
667
+
668
+ Another benefit is that you can define custom methods directly on the class
669
+ instead of through `auth_class_eval`:
670
+
671
+ ```rb
672
+ # app/lib/rodauth_admin.rb
673
+ class RodauthAdmin < Rodauth::Rails::Auth
674
+ configure do
675
+ # ...
676
+ end
677
+
678
+ def superadmin?
679
+ Role.where(account_id: session_id).any? { |role| role.name == "superadmin" }
680
+ end
681
+ end
682
+ ```
683
+ ```rb
684
+ # config/routes.rb
685
+ Rails.application.routes.draw do
686
+ constraints Rodauth::Rails.authenticated(:admin) { |rodauth| rodauth.superadmin? } do
687
+ mount Sidekiq::Web => "sidekiq"
688
+ end
689
+ end
690
+ ```
691
+
551
692
  ### Calling controller methods
552
693
 
553
694
  When using Rodauth before/after hooks or generally overriding your Rodauth
@@ -922,48 +1063,147 @@ end
922
1063
 
923
1064
  ## Testing
924
1065
 
925
- If you're writing system tests, it's generally better to go through the actual
926
- authentication flow with tools like Capybara, and to not use any stubbing.
927
-
928
- In functional and integration tests you can just make requests to Rodauth
929
- routes:
1066
+ System (browser) tests for Rodauth actions could look something like this:
930
1067
 
931
1068
  ```rb
932
- # test/controllers/posts_controller_test.rb
933
- class PostsControllerTest < ActionDispatch::IntegrationTest
934
- test "should require authentication" do
935
- get posts_url
936
- assert_redirected_to "/login"
1069
+ # test/system/authentication_test.rb
1070
+ require "test_helper"
1071
+
1072
+ class AuthenticationTest < ActionDispatch::SystemTestCase
1073
+ include ActiveJob::TestHelper
1074
+ driven_by :rack_test
1075
+
1076
+ test "creating and verifying an account" do
1077
+ create_account
1078
+ assert_match "An email has been sent to you with a link to verify your account", page.text
1079
+
1080
+ verify_account
1081
+ assert_match "Your account has been verified", page.text
1082
+ end
1083
+
1084
+ test "logging in and logging out" do
1085
+ create_account(verify: true)
1086
+
1087
+ logout
1088
+ assert_match "You have been logged out", page.text
937
1089
 
938
1090
  login
939
- get posts_url
1091
+ assert_match "You have been logged in", page.text
1092
+ end
1093
+
1094
+ private
1095
+
1096
+ def create_account(email: "user@example.com", password: "secret", verify: false)
1097
+ visit "/create-account"
1098
+ fill_in "Login", with: email
1099
+ fill_in "Password", with: password
1100
+ fill_in "Confirm Password", with: password
1101
+ click_on "Create Account"
1102
+ verify_account if verify
1103
+ end
1104
+
1105
+ def verify_account
1106
+ perform_enqueued_jobs # run enqueued email deliveries
1107
+ email = ActionMailer::Base.deliveries.last
1108
+ verify_account_link = email.body.to_s[/\S+verify-account\S+/]
1109
+ visit verify_account_link
1110
+ click_on "Verify Account"
1111
+ end
1112
+
1113
+ def login(email: "user@example.com", password: "secret")
1114
+ visit "/login"
1115
+ fill_in "Login", with: email
1116
+ fill_in "Password", with: password
1117
+ click_on "Login"
1118
+ end
1119
+
1120
+ def logout
1121
+ visit "/logout"
1122
+ click_on "Logout"
1123
+ end
1124
+ end
1125
+ ```
1126
+
1127
+ While request tests in JSON API mode with JWT tokens could look something like
1128
+ this:
1129
+
1130
+ ```rb
1131
+ # test/integration/authentication_test.rb
1132
+ require "test_helper"
1133
+
1134
+ class AuthenticationTest < ActionDispatch::IntegrationTest
1135
+ test "creating and verifying an account" do
1136
+ create_account
940
1137
  assert_response :success
1138
+ assert_match "An email has been sent to you with a link to verify your account", JSON.parse(body)["success"]
1139
+
1140
+ verify_account
1141
+ assert_response :success
1142
+ assert_match "Your account has been verified", JSON.parse(body)["success"]
1143
+ end
1144
+
1145
+ test "logging in and logging out" do
1146
+ create_account(verify: true)
941
1147
 
942
1148
  logout
943
- assert_redirected_to "/login"
1149
+ assert_response :success
1150
+ assert_match "You have been logged out", JSON.parse(body)["success"]
1151
+
1152
+ login
1153
+ assert_response :success
1154
+ assert_match "You have been logged in", JSON.parse(body)["success"]
944
1155
  end
945
1156
 
946
1157
  private
947
1158
 
948
- def login(login: "user@example.com", password: "secret")
949
- post "/create-account", params: {
950
- "login" => login,
951
- "password" => password,
952
- "password-confirm" => password,
953
- }
954
-
955
- post "/login", params: {
956
- "login" => login,
957
- "password" => password,
958
- }
1159
+ def create_account(email: "user@example.com", password: "secret", verify: false)
1160
+ post "/create-account", as: :json, params: { login: email, password: password, "password-confirm": password }
1161
+ verify_account if verify
1162
+ end
1163
+
1164
+ def verify_account
1165
+ perform_enqueued_jobs # run enqueued email deliveries
1166
+ email = ActionMailer::Base.deliveries.last
1167
+ verify_account_key = email.body.to_s[/verify-account\?key=(\S+)/, 1]
1168
+ post "/verify-account", as: :json, params: { key: verify_account_key }
1169
+ end
1170
+
1171
+ def login(email: "user@example.com", password: "secret")
1172
+ post "/login", as: :json, params: { login: email, password: password }
959
1173
  end
960
1174
 
961
1175
  def logout
962
- post "/logout"
1176
+ post "/logout", as: :json, headers: { "Authorization" => headers["Authorization"] }
963
1177
  end
964
1178
  end
965
1179
  ```
966
1180
 
1181
+ If you're delivering emails in the background, make sure to set Active Job
1182
+ queue adapter to `:test`:
1183
+
1184
+ ```rb
1185
+ # config/environments/test.rb
1186
+ Rails.application.configure do |config|
1187
+ # ...
1188
+ config.active_job.queue_adapter = :test
1189
+ # ...
1190
+ end
1191
+ ```
1192
+
1193
+ If you need to create the account manually, you can do it as follows:
1194
+
1195
+ ```rb
1196
+ account_id = DB[:accounts].insert(
1197
+ email: "user@example.com",
1198
+ status: "verified",
1199
+ )
1200
+
1201
+ DB[:account_password_hashes].insert(
1202
+ account_id: account_id,
1203
+ password_hash: BCrypt::Password.create("secret", cost: BCrpyt::Engine::MIN_COST),
1204
+ )
1205
+ ```
1206
+
967
1207
  ## Rodauth defaults
968
1208
 
969
1209
  rodauth-rails changes some of the default Rodauth settings for easier setup:
@@ -10,6 +10,15 @@ module Rodauth
10
10
  include ::ActiveRecord::Generators::Migration
11
11
  include MigrationHelpers
12
12
 
13
+ MAILER_VIEWS = %w[
14
+ email_auth
15
+ password_changed
16
+ reset_password
17
+ unlock_account
18
+ verify_account
19
+ verify_login_change
20
+ ]
21
+
13
22
  source_root "#{__dir__}/templates"
14
23
  namespace "rodauth:install"
15
24
 
@@ -47,6 +56,14 @@ module Rodauth
47
56
  template "app/models/account.rb"
48
57
  end
49
58
 
59
+ def create_mailer
60
+ template "app/mailers/rodauth_mailer.rb"
61
+
62
+ MAILER_VIEWS.each do |view|
63
+ template "app/views/rodauth_mailer/#{view}.text.erb"
64
+ end
65
+ end
66
+
50
67
  private
51
68
 
52
69
  def sequel_uri_scheme
@@ -2,10 +2,10 @@ class RodauthApp < Rodauth::Rails::App
2
2
  configure do
3
3
  # List of authentication features that are loaded.
4
4
  enable :create_account, :verify_account, :verify_account_grace_period,
5
- :login, :logout<%= ", :remember" unless jwt? %>,
5
+ :login, :logout<%= ", :remember" unless jwt? %><%= ", :json" if json? %><%= ", :jwt" if jwt? %>,
6
6
  :reset_password, :change_password, :change_password_notify,
7
7
  :change_login, :verify_login_change,
8
- :close_account<%= ", :json" if json? %><%= ", :jwt" if jwt? %>
8
+ :close_account
9
9
 
10
10
  # See the Rodauth documentation for the list of available config options:
11
11
  # http://rodauth.jeremyevans.net/documentation.html
@@ -23,6 +23,10 @@ class RodauthApp < Rodauth::Rails::App
23
23
 
24
24
  # Accept only JSON requests.
25
25
  only_json? true
26
+
27
+ # Handle login and password confirmation fields on the client side.
28
+ # require_password_confirmation? false
29
+ # require_login_confirmation? false
26
30
  <% end -%>
27
31
 
28
32
  # Specify the controller used for view rendering and CSRF verification.
@@ -54,35 +58,29 @@ class RodauthApp < Rodauth::Rails::App
54
58
  # already_logged_in { redirect login_redirect }
55
59
 
56
60
  # ==> Emails
57
- # Uncomment the lines below once you've imported mailer views.
58
- # create_reset_password_email do
59
- # RodauthMailer.reset_password(email_to, reset_password_email_link)
60
- # end
61
- # create_verify_account_email do
62
- # RodauthMailer.verify_account(email_to, verify_account_email_link)
63
- # end
64
- # create_verify_login_change_email do |login|
65
- # RodauthMailer.verify_login_change(login, verify_login_change_old_login, verify_login_change_new_login, verify_login_change_email_link)
66
- # end
67
- # create_password_changed_email do
68
- # RodauthMailer.password_changed(email_to)
61
+ # Use a custom mailer for delivering authentication emails.
62
+ create_reset_password_email do
63
+ RodauthMailer.reset_password(email_to, reset_password_email_link)
64
+ end
65
+ create_verify_account_email do
66
+ RodauthMailer.verify_account(email_to, verify_account_email_link)
67
+ end
68
+ create_verify_login_change_email do |login|
69
+ RodauthMailer.verify_login_change(login, verify_login_change_old_login, verify_login_change_new_login, verify_login_change_email_link)
70
+ end
71
+ create_password_changed_email do
72
+ RodauthMailer.password_changed(email_to)
73
+ end
74
+ # create_email_auth_email do
75
+ # RodauthMailer.email_auth(email_to, email_auth_email_link)
69
76
  # end
70
- # # create_email_auth_email do
71
- # # RodauthMailer.email_auth(email_to, email_auth_email_link)
72
- # # end
73
- # # create_unlock_account_email do
74
- # # RodauthMailer.unlock_account(email_to, unlock_account_email_link)
75
- # # end
76
- # send_email do |email|
77
- # # queue email delivery on the mailer after the transaction commits
78
- # db.after_commit { email.deliver_later }
77
+ # create_unlock_account_email do
78
+ # RodauthMailer.unlock_account(email_to, unlock_account_email_link)
79
79
  # end
80
-
81
- # In the meantime, you can tweak settings for emails created by Rodauth.
82
- # email_subject_prefix "[MyApp] "
83
- # email_from "noreply@myapp.com"
84
- # send_email(&:deliver_later)
85
- # reset_password_email_body { "Click here to reset your password: #{reset_password_email_link}" }
80
+ send_email do |email|
81
+ # queue email delivery on the mailer after the transaction commits
82
+ db.after_commit { email.deliver_later }
83
+ end
86
84
 
87
85
  # ==> Flash
88
86
  <% unless json? || jwt? -%>
@@ -151,7 +149,9 @@ class RodauthApp < Rodauth::Rails::App
151
149
  # verify_account_grace_period 3.days
152
150
  # reset_password_deadline_interval Hash[hours: 6]
153
151
  # verify_login_change_deadline_interval Hash[days: 2]
152
+ <% unless jwt? -%>
154
153
  # remember_deadline_interval Hash[days: 30]
154
+ <% end -%>
155
155
  end
156
156
 
157
157
  # ==> Multiple configurations
@@ -187,6 +187,8 @@ class RodauthApp < Rodauth::Rails::App
187
187
  # unless rodauth(:admin).logged_in?
188
188
  # rodauth(:admin).require_http_basic_auth
189
189
  # end
190
+ #
191
+ # r.pass # allow the Rails app to handle other "/admin/*" requests
190
192
  # end
191
193
  end
192
194
  end
@@ -1,4 +1,4 @@
1
- class <%= options[:name].camelize %>Mailer < ApplicationMailer
1
+ class RodauthMailer < ApplicationMailer
2
2
  def verify_account(recipient, email_link)
3
3
  @email_link = email_link
4
4
 
@@ -25,13 +25,13 @@ class <%= options[:name].camelize %>Mailer < ApplicationMailer
25
25
 
26
26
  # def email_auth(recipient, email_link)
27
27
  # @email_link = email_link
28
-
28
+ #
29
29
  # mail to: recipient
30
30
  # end
31
31
 
32
32
  # def unlock_account(recipient, email_link)
33
33
  # @email_link = email_link
34
-
34
+ #
35
35
  # mail to: recipient
36
36
  # end
37
37
  end
data/lib/rodauth/rails.rb CHANGED
@@ -8,6 +8,7 @@ module Rodauth
8
8
 
9
9
  # This allows the developer to avoid loading Rodauth at boot time.
10
10
  autoload :App, "rodauth/rails/app"
11
+ autoload :Auth, "rodauth/rails/auth"
11
12
 
12
13
  @app = nil
13
14
  @middleware = true
@@ -32,6 +33,15 @@ module Rodauth
32
33
  scope.rodauth(name)
33
34
  end
34
35
 
36
+ # routing constraint that requires authentication
37
+ def authenticated(name = nil, &condition)
38
+ lambda do |request|
39
+ rodauth = request.env.fetch ["rodauth", *name].join(".")
40
+ rodauth.require_authentication
41
+ rodauth.authenticated? && (condition.nil? || condition.call(rodauth))
42
+ end
43
+ end
44
+
35
45
  if ::Rails.gem_version >= Gem::Version.new("5.2")
36
46
  def secret_key_base
37
47
  ::Rails.application.secret_key_base
@@ -1,6 +1,5 @@
1
1
  require "roda"
2
- require "rodauth"
3
- require "rodauth/rails/feature"
2
+ require "rodauth/rails/auth"
4
3
 
5
4
  module Rodauth
6
5
  module Rails
@@ -11,38 +10,29 @@ module Rodauth
11
10
 
12
11
  plugin :hooks
13
12
  plugin :render, layout: false
13
+ plugin :pass
14
14
 
15
15
  unless Rodauth::Rails.api_only?
16
16
  require "rodauth/rails/app/flash"
17
17
  plugin Flash
18
18
  end
19
19
 
20
- def self.configure(name = nil, **options, &block)
21
- plugin :rodauth, name: name, csrf: false, flash: false, json: true, **options do
22
- # load the Rails integration
23
- enable :rails
20
+ def self.configure(*args, **options, &block)
21
+ auth_class = args.shift if args[0].is_a?(Class)
22
+ name = args.shift if args[0].is_a?(Symbol)
24
23
 
25
- # database functions are more complex to set up, so disable them by default
26
- use_database_authentication_functions? false
24
+ fail ArgumentError, "need to pass optional Rodauth::Auth subclass and optional configuration name" if args.any?
27
25
 
28
- # avoid having to set deadline values in column default values
29
- set_deadline_values? true
26
+ auth_class ||= Class.new(Rodauth::Rails::Auth)
30
27
 
31
- # use HMACs for additional security
32
- hmac_secret { Rodauth::Rails.secret_key_base }
33
-
34
- # evaluate user configuration
35
- instance_exec(&block)
28
+ plugin :rodauth, auth_class: auth_class, name: name, csrf: false, flash: false, json: true, **options do
29
+ instance_exec(&block) if block
36
30
  end
37
31
  end
38
32
 
39
33
  before do
40
- (opts[:rodauths] || {}).each do |name, _|
41
- if name
42
- env["rodauth.#{name}"] = rodauth(name)
43
- else
44
- env["rodauth"] = rodauth
45
- end
34
+ opts[:rodauths]&.each_key do |name|
35
+ env[["rodauth", *name].join(".")] = rodauth(name)
46
36
  end
47
37
  end
48
38
  end
@@ -3,20 +3,30 @@ module Rodauth
3
3
  class App
4
4
  # Roda plugin that extends middleware plugin by propagating response headers.
5
5
  module Middleware
6
- def self.load_dependencies(app)
7
- app.plugin :hooks
8
- end
9
-
10
6
  def self.configure(app)
11
- app.after do
12
- if response.empty? && response.headers.any?
13
- env["rodauth.rails.headers"] = response.headers
7
+ handle_result = -> (env, res) do
8
+ if headers = env.delete("rodauth.rails.headers")
9
+ res[1] = headers.merge(res[1])
14
10
  end
15
11
  end
16
12
 
17
- app.plugin :middleware, handle_result: -> (env, res) do
18
- if headers = env.delete("rodauth.rails.headers")
19
- res[1] = headers.merge(res[1])
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
20
30
  end
21
31
  end
22
32
  end
@@ -0,0 +1,40 @@
1
+ require "rodauth"
2
+ require "rodauth/rails/feature"
3
+
4
+ module Rodauth
5
+ module Rails
6
+ # Base auth class that applies some default configuration and supports
7
+ # multi-level inheritance.
8
+ class Auth < Rodauth::Auth
9
+ class << self
10
+ attr_writer :features
11
+ attr_writer :routes
12
+ attr_accessor :configuration
13
+ end
14
+
15
+ def self.inherited(auth_class)
16
+ super
17
+ auth_class.roda_class = Rodauth::Rails.app
18
+ auth_class.features = features.dup
19
+ auth_class.routes = routes.dup
20
+ auth_class.route_hash = route_hash.dup
21
+ auth_class.configuration = configuration.clone
22
+ auth_class.configuration.instance_variable_set(:@auth, auth_class)
23
+ end
24
+
25
+ # apply default configuration
26
+ configure do
27
+ enable :rails
28
+
29
+ # database functions are more complex to set up, so disable them by default
30
+ use_database_authentication_functions? false
31
+
32
+ # avoid having to set deadline values in column default values
33
+ set_deadline_values? true
34
+
35
+ # use HMACs for additional security
36
+ hmac_secret { Rodauth::Rails.secret_key_base }
37
+ end
38
+ end
39
+ end
40
+ end
@@ -9,11 +9,7 @@ module Rodauth
9
9
  end
10
10
 
11
11
  def rodauth(name = nil)
12
- if name
13
- request.env["rodauth.#{name}"]
14
- else
15
- request.env["rodauth"]
16
- end
12
+ request.env.fetch ["rodauth", *name].join(".")
17
13
  end
18
14
  end
19
15
  end
@@ -1,5 +1,5 @@
1
1
  module Rodauth
2
2
  module Rails
3
- VERSION = "0.9.1"
3
+ VERSION = "0.10.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.9"
20
+ spec.add_dependency "rodauth", "~> 2.11"
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.9.1
4
+ version: 0.10.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-02-10 00:00:00.000000000 Z
11
+ date: 2021-03-23 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.9'
39
+ version: '2.11'
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.9'
46
+ version: '2.11'
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: sequel-activerecord_connection
49
49
  requirement: !ruby/object:Gem::Requirement
@@ -111,7 +111,6 @@ files:
111
111
  - LICENSE.txt
112
112
  - README.md
113
113
  - lib/generators/rodauth/install_generator.rb
114
- - lib/generators/rodauth/mailer_generator.rb
115
114
  - lib/generators/rodauth/migration/account_expiration.erb
116
115
  - lib/generators/rodauth/migration/active_sessions.erb
117
116
  - lib/generators/rodauth/migration/audit_logging.erb
@@ -205,6 +204,7 @@ files:
205
204
  - lib/rodauth/rails/app.rb
206
205
  - lib/rodauth/rails/app/flash.rb
207
206
  - lib/rodauth/rails/app/middleware.rb
207
+ - lib/rodauth/rails/auth.rb
208
208
  - lib/rodauth/rails/controller_methods.rb
209
209
  - lib/rodauth/rails/feature.rb
210
210
  - lib/rodauth/rails/middleware.rb
@@ -1,37 +0,0 @@
1
- require "rails/generators/base"
2
-
3
- module Rodauth
4
- module Rails
5
- module Generators
6
- class MailerGenerator < ::Rails::Generators::Base
7
- source_root "#{__dir__}/templates"
8
- namespace "rodauth:mailer"
9
-
10
- VIEWS = %w[
11
- email_auth
12
- password_changed
13
- reset_password
14
- unlock_account
15
- verify_account
16
- verify_login_change
17
- ]
18
-
19
- class_option :name,
20
- desc: "The name for the mailer and the views directory",
21
- default: "rodauth"
22
-
23
- def copy_mailer
24
- template "app/mailers/rodauth_mailer.rb",
25
- "app/mailers/#{options[:name].underscore}_mailer.rb"
26
- end
27
-
28
- def copy_mailer_views
29
- VIEWS.each do |view|
30
- template "app/views/rodauth_mailer/#{view}.text.erb",
31
- "app/views/#{options[:name].underscore}_mailer/#{view}.text.erb"
32
- end
33
- end
34
- end
35
- end
36
- end
37
- end