rodauth-rails 0.11.0 → 0.12.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.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/README.md +36 -144
  4. data/lib/generators/rodauth/templates/app/views/rodauth/_global_logout_field.html.erb +1 -1
  5. data/lib/generators/rodauth/templates/app/views/rodauth/_login_confirm_field.html.erb +2 -2
  6. data/lib/generators/rodauth/templates/app/views/rodauth/_login_display.html.erb +2 -2
  7. data/lib/generators/rodauth/templates/app/views/rodauth/_login_field.html.erb +2 -2
  8. data/lib/generators/rodauth/templates/app/views/rodauth/_new_password_field.html.erb +2 -2
  9. data/lib/generators/rodauth/templates/app/views/rodauth/_otp_auth_code_field.html.erb +2 -2
  10. data/lib/generators/rodauth/templates/app/views/rodauth/_password_confirm_field.html.erb +2 -2
  11. data/lib/generators/rodauth/templates/app/views/rodauth/_password_field.html.erb +2 -2
  12. data/lib/generators/rodauth/templates/app/views/rodauth/_recovery_code_field.html.erb +2 -2
  13. data/lib/generators/rodauth/templates/app/views/rodauth/_sms_code_field.html.erb +2 -2
  14. data/lib/generators/rodauth/templates/app/views/rodauth/_sms_phone_field.html.erb +2 -2
  15. data/lib/generators/rodauth/templates/app/views/rodauth/_submit.html.erb +1 -1
  16. data/lib/generators/rodauth/templates/app/views/rodauth/otp_setup.html.erb +2 -2
  17. data/lib/generators/rodauth/templates/app/views/rodauth/remember.html.erb +1 -1
  18. data/lib/generators/rodauth/templates/app/views/rodauth/webauthn_remove.html.erb +1 -1
  19. data/lib/rodauth/rails/feature.rb +17 -230
  20. data/lib/rodauth/rails/feature/base.rb +62 -0
  21. data/lib/rodauth/rails/feature/callbacks.rb +61 -0
  22. data/lib/rodauth/rails/feature/csrf.rb +65 -0
  23. data/lib/rodauth/rails/feature/email.rb +30 -0
  24. data/lib/rodauth/rails/feature/instrumentation.rb +71 -0
  25. data/lib/rodauth/rails/feature/render.rb +41 -0
  26. data/lib/rodauth/rails/railtie.rb +0 -5
  27. data/lib/rodauth/rails/version.rb +1 -1
  28. metadata +8 -3
  29. data/lib/rodauth/rails/log_subscriber.rb +0 -34
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b8063be8ad00634114f74f0eb549c672e2b62cd1fa81cb7f124cc9cd12505e3f
4
- data.tar.gz: 6f466e29420f9e4bacb58c855e942cc20289d2c3fc69a12638b97628d25dbbfb
3
+ metadata.gz: 27d48e6bf86cf81b33f6b0282048c2fb6f16ec6602136e18de6ede5120cfd808
4
+ data.tar.gz: 2f79498ff25a42131a5ead77f3d4adf05152bc85f271c8b985f0f9fa8c04b503
5
5
  SHA512:
6
- metadata.gz: 8cc0af59c6ce29837fbc8a3401d456fd407ef76b74493b08ee9b4f2dfc8807d4a95c86f9bb0266401013d5162c009d46b7d07e3f741654af2cc267c0ee2c135e
7
- data.tar.gz: 78c098dbaed458d5764ca2e7ee61f4710e01b2386d0cc04831b1732b9883d76c4b9f56c35c0a1e557c40951086d72bb0ed264f769313c1b45be30b2dd760024a
6
+ metadata.gz: 8a0c44b54d304d4dfb2a205d41a5ac360e483209229fa49e767f9eaa595434b291661e283110f3ee39a8fbc17a4ad2d82f90a6e4545ca4112852ee50a35aa8da
7
+ data.tar.gz: 52bb16489dd97777f7ff2359be9014a2c55c7537b8d4449621eb95ef3b7f0030febcd06caa811d406db1fb24fcc884d22c7460a36a94255133ce261a2bbeb68d
data/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## 0.12.0 (2021-05-15)
2
+
3
+ * Include total view render time in logs for Rodauth requests (@janko)
4
+
5
+ * Instrument redirects (@janko)
6
+
7
+ * Instrument Rodauth requests on `action_controller` namespace (@janko)
8
+
9
+ * Update templates for Boostrap 5 compatibility (@janko)
10
+
11
+ * Log request parameters for Rodauth requests (@janko)
12
+
1
13
  ## 0.11.0 (2021-05-06)
2
14
 
3
15
  * Add controller-like logging for requests to Rodauth endpoints (@janko)
data/README.md CHANGED
@@ -61,7 +61,7 @@ documentation][hmac] for instructions on how to safely transition, or just set
61
61
  Add the gem to your Gemfile:
62
62
 
63
63
  ```rb
64
- gem "rodauth-rails", "~> 0.10"
64
+ gem "rodauth-rails", "~> 0.12"
65
65
 
66
66
  # gem "jwt", require: false # for JWT feature
67
67
  # gem "rotp", require: false # for OTP feature
@@ -86,132 +86,22 @@ $ rails generate rodauth:install --jwt # token authentication via the "Authoriza
86
86
  $ bundle add jwt
87
87
  ```
88
88
 
89
- The generator will create the following files:
89
+ This generator will create a Rodauth app with common authentication features
90
+ enabled, a database migration with tables required by those features, a mailer
91
+ with default templates, and a few other files.
90
92
 
91
- * Rodauth migration at `db/migrate/*_create_rodauth.rb`
92
- * Rodauth initializer at `config/initializers/rodauth.rb`
93
- * Sequel initializer at `config/initializers/sequel.rb` for ActiveRecord integration
94
- * Rodauth app at `app/lib/rodauth_app.rb`
95
- * Rodauth controller at `app/controllers/rodauth_controller.rb`
96
- * Account model at `app/models/account.rb`
97
- * Rodauth mailer at `app/mailers/rodauth_mailer.rb` with views
93
+ Feel free to remove any features you don't need, along with their corresponding
94
+ tables. Afterwards, run the migration:
98
95
 
99
- ### Migration
100
-
101
- The migration file creates tables required by Rodauth. You're encouraged to
102
- review the migration, and modify it to only create tables for features you
103
- intend to use.
104
-
105
- ```rb
106
- # db/migrate/*_create_rodauth.rb
107
- class CreateRodauth < ActiveRecord::Migration
108
- def change
109
- create_table :accounts do |t| ... end
110
- create_table :account_password_hashes do |t| ... end
111
- create_table :account_password_reset_keys do |t| ... end
112
- create_table :account_verification_keys do |t| ... end
113
- create_table :account_login_change_keys do |t| ... end
114
- create_table :account_remember_keys do |t| ... end
115
- end
116
- end
117
- ```
118
-
119
- Once you're done, you can run the migration:
120
-
121
- ```
96
+ ```sh
122
97
  $ rails db:migrate
123
98
  ```
124
99
 
125
- ### Rodauth initializer
126
-
127
- The Rodauth initializer assigns the constant for your Rodauth app, which will
128
- be called by the Rack middleware that's added in front of your Rails router.
129
-
130
- ```rb
131
- # config/initializers/rodauth.rb
132
- Rodauth::Rails.configure do |config|
133
- config.app = "RodauthApp"
134
- end
135
- ```
136
-
137
- ### Sequel initializer
138
-
139
- Rodauth uses [Sequel] for database interaction. If you're using ActiveRecord,
140
- an additional initializer will be created which configures Sequel to use the
141
- ActiveRecord connection.
142
-
143
- ```rb
144
- # config/initializers/sequel.rb
145
- require "sequel/core"
146
-
147
- # initialize Sequel and have it reuse Active Record's database connection
148
- DB = Sequel.connect("postgresql://", extensions: :activerecord_connection)
149
- ```
150
-
151
- ### Rodauth app
152
-
153
- Your Rodauth app is created in the `app/lib/` directory, and comes with a
154
- default set of authentication features enabled, as well as extensive examples
155
- on ways you can configure authentication behaviour.
156
-
157
- ```rb
158
- # app/lib/rodauth_app.rb
159
- class RodauthApp < Rodauth::Rails::App
160
- configure do
161
- # authentication configuration
162
- end
163
-
164
- route do |r|
165
- # request handling
166
- end
167
- end
168
- ```
169
-
170
- ### Controller
171
-
172
- Your Rodauth app will by default use `RodauthController` for view rendering,
173
- CSRF protection, and running controller callbacks and rescue handlers around
174
- Rodauth actions.
175
-
176
- ```rb
177
- # app/controllers/rodauth_controller.rb
178
- class RodauthController < ApplicationController
179
- end
180
- ```
181
-
182
- ### Account model
183
-
184
- Rodauth stores user accounts in the `accounts` table, so the generator will
185
- also create an `Account` model for custom use.
186
-
187
- ```rb
188
- # app/models/account.rb
189
- class Account < ApplicationRecord
190
- end
191
- ```
192
-
193
- ### Rodauth mailer
194
-
195
- The default Rodauth app is configured to use `RodauthMailer` mailer
196
- for sending authentication emails.
197
-
198
- ```rb
199
- # app/mailers/rodauth_mailer.rb
200
- class RodauthMailer < ApplicationMailer
201
- def verify_account(recipient, email_link) ... end
202
- def reset_password(recipient, email_link) ... end
203
- def verify_login_change(recipient, old_login, new_login, email_link) ... end
204
- def password_changed(recipient) ... end
205
- # def email_auth(recipient, email_link) ... end
206
- # def unlock_account(recipient, email_link) ... end
207
- end
208
- ```
209
-
210
100
  ## Usage
211
101
 
212
102
  ### Routes
213
103
 
214
- We can see the list of routes our Rodauth middleware handles:
104
+ You can see the list of routes our Rodauth middleware handles:
215
105
 
216
106
  ```sh
217
107
  $ rails rodauth:routes
@@ -233,7 +123,7 @@ Routes handled by RodauthApp:
233
123
  /close-account rodauth.close_account_path
234
124
  ```
235
125
 
236
- Using this information, we could add some basic authentication links to our
126
+ Using this information, you can add some basic authentication links to your
237
127
  navigation header:
238
128
 
239
129
  ```erb
@@ -264,7 +154,7 @@ end
264
154
 
265
155
  ### Current account
266
156
 
267
- To be able to fetch currently authenticated account, let's define a
157
+ To be able to fetch currently authenticated account, you can define a
268
158
  `#current_account` method that fetches the account id from session and
269
159
  retrieves the corresponding account record:
270
160
 
@@ -281,11 +171,11 @@ class ApplicationController < ActionController::Base
281
171
  rodauth.logout
282
172
  rodauth.login_required
283
173
  end
284
- helper_method :current_account # skip if inheriting from ActionController:API
174
+ helper_method :current_account # skip if inheriting from ActionController::API
285
175
  end
286
176
  ```
287
177
 
288
- This allows us to access the current account in controllers and views:
178
+ This allows you to access the current account in controllers and views:
289
179
 
290
180
  ```erb
291
181
  <p>Authenticated as: <%= current_account.email %></p>
@@ -293,9 +183,9 @@ This allows us to access the current account in controllers and views:
293
183
 
294
184
  ### Requiring authentication
295
185
 
296
- We'll likely want to require authentication for certain parts of our app,
297
- redirecting the user to the login page if they're not logged in. We can do this
298
- in our Rodauth app's routing block, which helps keep the authentication logic
186
+ You'll likely want to require authentication for certain parts of your app,
187
+ redirecting the user to the login page if they're not logged in. You can do this
188
+ in your Rodauth app's routing block, which helps keep the authentication logic
299
189
  encapsulated:
300
190
 
301
191
  ```rb
@@ -314,7 +204,7 @@ class RodauthApp < Rodauth::Rails::App
314
204
  end
315
205
  ```
316
206
 
317
- We can also require authentication at the controller layer:
207
+ You can also require authentication at the controller layer:
318
208
 
319
209
  ```rb
320
210
  # app/controllers/application_controller.rb
@@ -341,8 +231,8 @@ end
341
231
 
342
232
  #### Routing constraints
343
233
 
344
- You can also require authentication at the Rails router level by
345
- using a built-in `authenticated` routing constraint:
234
+ In some cases it makes sense to require authentication at the Rails router
235
+ level. You can do this via the built-in `authenticated` routing constraint:
346
236
 
347
237
  ```rb
348
238
  # config/routes.rb
@@ -404,7 +294,7 @@ This will generate views for the default set of Rodauth features into the
404
294
  `RodauthController`.
405
295
 
406
296
  You can pass a list of Rodauth features to the generator to create views for
407
- these features (this will not remove any existing views):
297
+ these features (this will not remove or overwrite any existing views):
408
298
 
409
299
  ```sh
410
300
  $ rails generate rodauth:views login create_account lockout otp
@@ -546,7 +436,7 @@ end
546
436
  ### Multiple configurations
547
437
 
548
438
  If you need to handle multiple types of accounts that require different
549
- authentication logic, you can create different configurations for them:
439
+ authentication logic, you can create additional configurations for them:
550
440
 
551
441
  ```rb
552
442
  # app/lib/rodauth_app.rb
@@ -656,8 +546,8 @@ class RodauthAdmin < RodauthBase # inherit common settings
656
546
  end
657
547
  ```
658
548
 
659
- Another benefit is that you can define custom methods directly on the class
660
- instead of through `auth_class_eval`:
549
+ Another benefit of explicit classes is that you can define custom methods
550
+ directly at the class level instead of inside an `auth_class_eval`:
661
551
 
662
552
  ```rb
663
553
  # app/lib/rodauth_admin.rb
@@ -722,7 +612,7 @@ rodauth.setup_account_verification
722
612
  rodauth.close_account
723
613
  ```
724
614
 
725
- This Rodauth instance will be initialized with basic Rack env that allows is it
615
+ This Rodauth instance will be initialized with basic Rack env that allows it
726
616
  to generate URLs, using `config.action_mailer.default_url_options` options.
727
617
 
728
618
  ## How it works
@@ -834,7 +724,7 @@ class RodauthApp < Rodauth::Rails::App
834
724
  configure do
835
725
  # ...
836
726
  enable :json
837
- only_json? true # accept only JSON requests
727
+ only_json? true # accept only JSON requests (optional)
838
728
  # ...
839
729
  end
840
730
  end
@@ -855,7 +745,7 @@ class RodauthApp < Rodauth::Rails::App
855
745
  # ...
856
746
  enable :jwt
857
747
  jwt_secret "<YOUR_SECRET_KEY>" # store the JWT secret in a safe place
858
- only_json? true # accept only JSON requests
748
+ only_json? true # accept only JSON requests (optional)
859
749
  # ...
860
750
  end
861
751
  end
@@ -935,7 +825,8 @@ end
935
825
  <%= link_to "Login via Facebook", "/auth/facebook" %>
936
826
  ```
937
827
 
938
- Let's implement the OmniAuth callback endpoint on our Rodauth controller:
828
+ Finally, let's implement the OmniAuth callback endpoint on our Rodauth
829
+ controller:
939
830
 
940
831
  ```rb
941
832
  # config/routes.rb
@@ -988,11 +879,8 @@ end
988
879
 
989
880
  ## Configuring
990
881
 
991
- For the list of configuration methods provided by Rodauth, see the [feature
992
- documentation].
993
-
994
- The `rails` feature rodauth-rails loads is customizable as well, here is the
995
- list of its configuration methods:
882
+ The `rails` feature rodauth-rails loads provides the following configuration
883
+ methods:
996
884
 
997
885
  | Name | Description |
998
886
  | :---- | :---------- |
@@ -1019,12 +907,16 @@ Rodauth::Rails.configure do |config|
1019
907
  end
1020
908
  ```
1021
909
 
910
+ For the list of configuration methods provided by Rodauth, see the [feature
911
+ documentation].
912
+
1022
913
  ## Custom extensions
1023
914
 
1024
915
  When developing custom extensions for Rodauth inside your Rails project, it's
1025
- better to use plain modules (at least in the beginning), because Rodauth
1026
- feature design doesn't yet support Zeitwerk reloading well. Here is
1027
- an example of an LDAP authentication extension that uses the
916
+ probably better to use plain modules, at least in the beginning, as Rodauth
917
+ feature design doesn't yet work well with Zeitwerk reloading.
918
+
919
+ Here is an example of an LDAP authentication extension that uses the
1028
920
  [simple_ldap_authenticator] gem.
1029
921
 
1030
922
  ```rb
@@ -1,4 +1,4 @@
1
- <div class="form-group">
1
+ <div class="form-group mb-3">
2
2
  <div class="form-check">
3
3
  <%%= check_box_tag rodauth.global_logout_param, "t", false, id: "global-logout", class: "form-check-input" %>
4
4
  <%%= label_tag "global-logout", "Logout all Logged In Sessons?", class: "form-check-label" %>
@@ -1,4 +1,4 @@
1
- <div class="form-group">
2
- <%%= label_tag "login-confirm", "Confirm Login" %>
1
+ <div class="form-group mb-3">
2
+ <%%= label_tag "login-confirm", "Confirm Login", class: "form-label" %>
3
3
  <%%= render "field", name: rodauth.login_confirm_param, id: "login-confirm", type: :email, autocomplete: "email" %>
4
4
  </div>
@@ -1,4 +1,4 @@
1
- <div class="form-group">
2
- <%%= label_tag "login", "Login" %>
1
+ <div class="form-group mb-3">
2
+ <%%= label_tag "login", "Login", class: "form-label" %>
3
3
  <%%= email_field_tag rodauth.login_param, params[rodauth.login_param], id: "login", readonly: true, class: "form-control-plaintext" %>
4
4
  </div>
@@ -1,4 +1,4 @@
1
- <div class="form-group">
2
- <%%= label_tag "login", "Login" %>
1
+ <div class="form-group mb-3">
2
+ <%%= label_tag "login", "Login", class: "form-label" %>
3
3
  <%%= render "field", name: rodauth.login_param, id: "login", type: :email, autocomplete: "email" %>
4
4
  </div>
@@ -1,4 +1,4 @@
1
- <div class="form-group">
2
- <%%= label_tag "new-password", "New Password" %>
1
+ <div class="form-group mb-3">
2
+ <%%= label_tag "new-password", "New Password", class: "form-label" %>
3
3
  <%%= render "field", name: rodauth.new_password_param, id: "new-password", type: "password", value: "", autocomplete: "new-password" %>
4
4
  </div>
@@ -1,5 +1,5 @@
1
- <div class="form-group">
2
- <%%= label_tag "otp-auth-code", "Authentication Code" %>
1
+ <div class="form-group mb-3">
2
+ <%%= label_tag "otp-auth-code", "Authentication Code", class: "form-label" %>
3
3
  <div class="row">
4
4
  <div class="col-sm-3">
5
5
  <%%= render "field", name: rodauth.otp_auth_param, id: "otp-auth-code", value: "", autocomplete: "off", inputmode: "numeric" %>
@@ -1,4 +1,4 @@
1
- <div class="form-group">
2
- <%%= label_tag "password-confirm", "Confirm Password" %>
1
+ <div class="form-group mb-3">
2
+ <%%= label_tag "password-confirm", "Confirm Password", class: "form-label" %>
3
3
  <%%= render "field", name: rodauth.password_confirm_param, id: "password-confirm", type: :password, value: "", autocomplete: "new-password" %>
4
4
  </div>
@@ -1,4 +1,4 @@
1
- <div class="form-group">
2
- <%%= label_tag "password", "Password" %>
1
+ <div class="form-group mb-3">
2
+ <%%= label_tag "password", "Password", class: "form-label" %>
3
3
  <%%= render "field", name: rodauth.password_param, id: "password", type: :password, value: "", autocomplete: rodauth.password_field_autocomplete_value %>
4
4
  </div>
@@ -1,4 +1,4 @@
1
- <div class="form-group">
2
- <%%= label_tag "recovery_code", "Recovery Code" %>
1
+ <div class="form-group mb-3">
2
+ <%%= label_tag "recovery_code", "Recovery Code", class: "form-label" %>
3
3
  <%%= render "field", name: rodauth.recovery_codes_param, id: "recovery_code", value: "", autocomplete: "off" %>
4
4
  </div>
@@ -1,5 +1,5 @@
1
- <div class="form-group">
2
- <%%= label_tag "sms-code", "SMS Code" %>
1
+ <div class="form-group mb-3">
2
+ <%%= label_tag "sms-code", "SMS Code", class: "form-label" %>
3
3
  <div class="row">
4
4
  <div class="col-sm-3">
5
5
  <%%= render "field", name: rodauth.sms_code_param, id: "sms-code", value: "", autocomplete: "one-time-code", inputmode: "numeric" %>
@@ -1,5 +1,5 @@
1
- <div class="form-group">
2
- <%%= label_tag "sms-phone", "Phone Number" %>
1
+ <div class="form-group mb-3">
2
+ <%%= label_tag "sms-phone", "Phone Number", class: "form-label" %>
3
3
  <div class="row">
4
4
  <div class="col-sm-3">
5
5
  <%%= render "field", name: rodauth.sms_phone_param, id: "sms-phone", type: :tel, autocomplete: "tel" %>
@@ -1,3 +1,3 @@
1
- <div class="form-group">
1
+ <div class="form-group mb-3">
2
2
  <%%= submit_tag local_assigns[:value], name: local_assigns[:name], class: local_assigns[:class] || "btn btn-primary" %>
3
3
  </div>
@@ -2,14 +2,14 @@
2
2
  <%%= hidden_field_tag rodauth.otp_setup_param, rodauth.otp_user_key, id: "otp-key" %>
3
3
  <%%= hidden_field_tag rodauth.otp_setup_raw_param, rodauth.otp_key, id: "otp-hmac-secret" if rodauth.otp_keys_use_hmac? %>
4
4
 
5
- <div class="form-group">
5
+ <div class="form-group mb-3">
6
6
  <p>Secret: <%%= rodauth.otp_user_key %></p>
7
7
  <p>Provisioning URL: <%%= rodauth.otp_provisioning_uri %></p>
8
8
  </div>
9
9
 
10
10
  <div class="row">
11
11
  <div class="col-lg-6 col-lg">
12
- <div class="form-group">
12
+ <div class="form-group mb-3">
13
13
  <p><%%= rodauth.otp_qr_code.html_safe %></p>
14
14
  </div>
15
15
  </div>
@@ -1,5 +1,5 @@
1
1
  <%%= form_tag rodauth.remember_path, method: :post do %>
2
- <fieldset class="form-group">
2
+ <fieldset class="form-group mb-3">
3
3
  <div class="form-check">
4
4
  <%%= radio_button_tag rodauth.remember_param, rodauth.remember_remember_param_value, false, id: "remember-remember", class: "form-check-input" %>
5
5
  <%%= label_tag "remember-remember", "Remember Me", class: "form-check-label" %>
@@ -1,6 +1,6 @@
1
1
  <%%= form_tag rodauth.webauthn_remove_path, method: :post, id: "webauthn-remove-form" do %>
2
2
  <%%= render "password_field" if rodauth.two_factor_modifications_require_password? %>
3
- <fieldset class="form-group">
3
+ <fieldset class="form-group mb-3">
4
4
  <%% (usage = rodauth.account_webauthn_usage).each do |id, last_use| %>
5
5
  <div class="form-check">
6
6
  <%%= render "field", name: rodauth.webauthn_remove_param, id: "webauthn-remove-#{id}", type: :radio, class: "form-check-input", skip_error_message: true, value: id, required: false %>
@@ -1,234 +1,21 @@
1
1
  module Rodauth
2
2
  Feature.define(:rails) do
3
- depends :email_base
4
-
5
- # List of overridable methods.
6
- auth_methods(
7
- :rails_render,
8
- :rails_csrf_tag,
9
- :rails_csrf_param,
10
- :rails_csrf_token,
11
- :rails_check_csrf!,
12
- :rails_controller,
13
- )
14
-
15
- auth_cached_method :rails_controller_instance
16
-
17
- # Renders templates with layout. First tries to render a user-defined
18
- # template, otherwise falls back to Rodauth's template.
19
- def view(page, *)
20
- rails_render(action: page.tr("-", "_"), layout: true) ||
21
- rails_render(html: super.html_safe, layout: true)
22
- end
23
-
24
- # Renders templates without layout. First tries to render a user-defined
25
- # template or partial, otherwise falls back to Rodauth's template.
26
- def render(page)
27
- rails_render(partial: page.tr("-", "_"), layout: false) ||
28
- rails_render(action: page.tr("-", "_"), layout: false) ||
29
- super.html_safe
30
- end
31
-
32
- # Render Rails CSRF tags in Rodauth templates.
33
- def csrf_tag(*)
34
- rails_csrf_tag
35
- end
36
-
37
- # Verify Rails' authenticity token.
38
- def check_csrf
39
- rails_check_csrf!
40
- end
41
-
42
- # Have Rodauth call #check_csrf automatically.
43
- def check_csrf?
44
- true
45
- end
46
-
47
- # Reset Rails session to protect from session fixation attacks.
48
- def clear_session
49
- rails_controller_instance.reset_session
50
- end
51
-
52
- # Default the flash error key to Rails' default :alert.
53
- def flash_error_key
54
- :alert
55
- end
56
-
57
- # Evaluates the block in context of a Rodauth controller instance.
58
- def rails_controller_eval(&block)
59
- rails_controller_instance.instance_exec(&block)
60
- end
61
-
62
- def button(*)
63
- super.html_safe
64
- end
65
-
66
- delegate :rails_routes, :rails_request, to: :scope
67
-
68
- private
69
-
70
- # Runs controller callbacks and rescue handlers around Rodauth actions.
71
- def _around_rodauth(&block)
72
- result = nil
73
-
74
- rails_instrument_request do
75
- rails_controller_rescue do
76
- rails_controller_callbacks do
77
- result = catch(:halt) { super(&block) }
78
- end
79
- end
80
-
81
- result = handle_rails_controller_response(result)
82
- end
83
-
84
- throw :halt, result if result
85
- end
86
-
87
- # Handles controller rendering a response or setting response headers.
88
- def handle_rails_controller_response(result)
89
- if rails_controller_instance.performed?
90
- rails_controller_response
91
- elsif result
92
- result[1].merge!(rails_controller_instance.response.headers)
93
- result
94
- end
95
- end
96
-
97
- # Runs any #(before|around|after)_action controller callbacks.
98
- def rails_controller_callbacks
99
- # don't verify CSRF token as part of callbacks, Rodauth will do that
100
- rails_controller_forgery_protection { false }
101
-
102
- rails_controller_instance.run_callbacks(:process_action) do
103
- # turn the setting back to default so that form tags generate CSRF tags
104
- rails_controller_forgery_protection { rails_controller.allow_forgery_protection }
105
-
106
- yield
107
- end
108
- end
109
-
110
- # Runs any registered #rescue_from controller handlers.
111
- def rails_controller_rescue
112
- yield
113
- rescue Exception => exception
114
- rails_controller_instance.rescue_with_handler(exception) || raise
115
-
116
- unless rails_controller_instance.performed?
117
- raise Rodauth::Rails::Error, "rescue_from handler didn't write any response"
118
- end
119
- end
120
-
121
- def rails_instrument_request
122
- ActiveSupport::Notifications.instrument("start_processing.rodauth", rodauth: self)
123
- ActiveSupport::Notifications.instrument("process_request.rodauth", rodauth: self) do |payload|
124
- begin
125
- status, headers, body = yield
126
- payload[:status] = status || 404
127
- payload[:headers] = headers
128
- payload[:body] = body
129
- ensure
130
- rails_controller_instance.send(:append_info_to_payload, payload)
131
- end
132
- end
133
- end
134
-
135
- # Returns Roda response from controller response if set.
136
- def rails_controller_response
137
- controller_response = rails_controller_instance.response
138
-
139
- response.status = controller_response.status
140
- response.headers.merge! controller_response.headers
141
- response.write controller_response.body
142
-
143
- response.finish
144
- end
145
-
146
- # Create emails with ActionMailer which uses configured delivery method.
147
- def create_email_to(to, subject, body)
148
- Mailer.create_email(to: to, from: email_from, subject: "#{email_subject_prefix}#{subject}", body: body)
149
- end
150
-
151
- # Delivers the given email.
152
- def send_email(email)
153
- email.deliver_now
154
- end
155
-
156
- # Calls the Rails renderer, returning nil if a template is missing.
157
- def rails_render(*args)
158
- return if rails_api_controller?
159
-
160
- rails_controller_instance.render_to_string(*args)
161
- rescue ActionView::MissingTemplate
162
- nil
163
- end
164
-
165
- # Calls the controller to verify the authenticity token.
166
- def rails_check_csrf!
167
- rails_controller_instance.send(:verify_authenticity_token)
168
- end
169
-
170
- # Hidden tag with Rails CSRF token inserted into Rodauth templates.
171
- def rails_csrf_tag
172
- %(<input type="hidden" name="#{rails_csrf_param}" value="#{rails_csrf_token}">)
173
- end
174
-
175
- # The request parameter under which to send the Rails CSRF token.
176
- def rails_csrf_param
177
- rails_controller.request_forgery_protection_token
178
- end
179
-
180
- # The Rails CSRF token value inserted into Rodauth templates.
181
- def rails_csrf_token
182
- rails_controller_instance.send(:form_authenticity_token)
183
- end
184
-
185
- # allows/disables forgery protection
186
- def rails_controller_forgery_protection(&value)
187
- return if rails_api_controller?
188
-
189
- rails_controller_instance.allow_forgery_protection = value.call
190
- end
191
-
192
- # Instances of the configured controller with current request's env hash.
193
- def _rails_controller_instance
194
- controller = rails_controller.new
195
- prepare_rails_controller(controller, rails_request)
196
- controller
197
- end
198
-
199
- if ActionPack.version >= Gem::Version.new("5.0")
200
- def prepare_rails_controller(controller, rails_request)
201
- controller.set_request! rails_request
202
- controller.set_response! rails_controller.make_response!(rails_request)
203
- end
204
- else
205
- def prepare_rails_controller(controller, rails_request)
206
- controller.send(:set_response!, rails_request)
207
- controller.instance_variable_set(:@_request, rails_request)
208
- end
209
- end
210
-
211
- def rails_api_controller?
212
- defined?(ActionController::API) && rails_controller <= ActionController::API
213
- end
214
-
215
- def rails_controller
216
- if only_json? && Rodauth::Rails.api_only?
217
- ActionController::API
218
- else
219
- ActionController::Base
220
- end
221
- end
222
-
223
- # ActionMailer subclass for correct email delivering.
224
- class Mailer < ActionMailer::Base
225
- def create_email(**options)
226
- mail(**options)
227
- end
228
- end
3
+ # Assign feature and feature configuration to constants for introspection.
4
+ Rodauth::Rails::Feature = self
5
+ Rodauth::Rails::FeatureConfiguration = self.configuration
6
+
7
+ require "rodauth/rails/feature/base"
8
+ require "rodauth/rails/feature/callbacks"
9
+ require "rodauth/rails/feature/csrf"
10
+ require "rodauth/rails/feature/render"
11
+ require "rodauth/rails/feature/email"
12
+ require "rodauth/rails/feature/instrumentation"
13
+
14
+ include Rodauth::Rails::Feature::Base
15
+ include Rodauth::Rails::Feature::Callbacks
16
+ include Rodauth::Rails::Feature::Csrf
17
+ include Rodauth::Rails::Feature::Render
18
+ include Rodauth::Rails::Feature::Email
19
+ include Rodauth::Rails::Feature::Instrumentation
229
20
  end
230
-
231
- # Assign feature and feature configuration to constants for introspection.
232
- Rails::Feature = FEATURES[:rails]
233
- Rails::FeatureConfiguration = FEATURES[:rails].configuration
234
21
  end
@@ -0,0 +1,62 @@
1
+ module Rodauth
2
+ module Rails
3
+ module Feature
4
+ module Base
5
+ def self.included(feature)
6
+ feature.auth_methods :rails_controller
7
+ feature.auth_cached_method :rails_controller_instance
8
+ end
9
+
10
+ # Reset Rails session to protect from session fixation attacks.
11
+ def clear_session
12
+ rails_controller_instance.reset_session
13
+ end
14
+
15
+ # Default the flash error key to Rails' default :alert.
16
+ def flash_error_key
17
+ :alert
18
+ end
19
+
20
+ # Evaluates the block in context of a Rodauth controller instance.
21
+ def rails_controller_eval(&block)
22
+ rails_controller_instance.instance_exec(&block)
23
+ end
24
+
25
+ delegate :rails_routes, :rails_request, to: :scope
26
+
27
+ private
28
+
29
+ # Instances of the configured controller with current request's env hash.
30
+ def _rails_controller_instance
31
+ controller = rails_controller.new
32
+ prepare_rails_controller(controller, rails_request)
33
+ controller
34
+ end
35
+
36
+ if ActionPack.version >= Gem::Version.new("5.0")
37
+ def prepare_rails_controller(controller, rails_request)
38
+ controller.set_request! rails_request
39
+ controller.set_response! rails_controller.make_response!(rails_request)
40
+ end
41
+ else
42
+ def prepare_rails_controller(controller, rails_request)
43
+ controller.send(:set_response!, rails_request)
44
+ controller.instance_variable_set(:@_request, rails_request)
45
+ end
46
+ end
47
+
48
+ def rails_api_controller?
49
+ defined?(ActionController::API) && rails_controller <= ActionController::API
50
+ end
51
+
52
+ def rails_controller
53
+ if only_json? && Rodauth::Rails.api_only?
54
+ ActionController::API
55
+ else
56
+ ActionController::Base
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,61 @@
1
+ module Rodauth
2
+ module Rails
3
+ module Feature
4
+ module Callbacks
5
+ private
6
+
7
+ # Runs controller callbacks and rescue handlers around Rodauth actions.
8
+ def _around_rodauth(&block)
9
+ result = nil
10
+
11
+ rails_controller_rescue do
12
+ rails_controller_callbacks do
13
+ result = catch(:halt) { super(&block) }
14
+ end
15
+ end
16
+
17
+ result = handle_rails_controller_response(result)
18
+
19
+ throw :halt, result if result
20
+ end
21
+
22
+ # Runs any #(before|around|after)_action controller callbacks.
23
+ def rails_controller_callbacks(&block)
24
+ rails_controller_instance.run_callbacks(:process_action, &block)
25
+ end
26
+
27
+ # Runs any registered #rescue_from controller handlers.
28
+ def rails_controller_rescue
29
+ yield
30
+ rescue Exception => exception
31
+ rails_controller_instance.rescue_with_handler(exception) || raise
32
+
33
+ unless rails_controller_instance.performed?
34
+ raise Rodauth::Rails::Error, "rescue_from handler didn't write any response"
35
+ end
36
+ end
37
+
38
+ # Handles controller rendering a response or setting response headers.
39
+ def handle_rails_controller_response(result)
40
+ if rails_controller_instance.performed?
41
+ rails_controller_response
42
+ elsif result
43
+ result[1].merge!(rails_controller_instance.response.headers)
44
+ result
45
+ end
46
+ end
47
+
48
+ # Returns Roda response from controller response if set.
49
+ def rails_controller_response
50
+ controller_response = rails_controller_instance.response
51
+
52
+ response.status = controller_response.status
53
+ response.headers.merge! controller_response.headers
54
+ response.write controller_response.body
55
+
56
+ response.finish
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,65 @@
1
+ module Rodauth
2
+ module Rails
3
+ module Feature
4
+ module Csrf
5
+ def self.included(feature)
6
+ feature.auth_methods(
7
+ :rails_csrf_tag,
8
+ :rails_csrf_param,
9
+ :rails_csrf_token,
10
+ :rails_check_csrf!,
11
+ )
12
+ end
13
+
14
+ # Render Rails CSRF tags in Rodauth templates.
15
+ def csrf_tag(*)
16
+ rails_csrf_tag
17
+ end
18
+
19
+ # Verify Rails' authenticity token.
20
+ def check_csrf
21
+ rails_check_csrf!
22
+ end
23
+
24
+ # Have Rodauth call #check_csrf automatically.
25
+ def check_csrf?
26
+ true
27
+ end
28
+
29
+ private
30
+
31
+ def rails_controller_callbacks
32
+ return super if rails_api_controller?
33
+
34
+ # don't verify CSRF token as part of callbacks, Rodauth will do that
35
+ rails_controller_instance.allow_forgery_protection = false
36
+ super do
37
+ # turn the setting back to default so that form tags generate CSRF tags
38
+ rails_controller_instance.allow_forgery_protection = rails_controller.allow_forgery_protection
39
+ yield
40
+ end
41
+ end
42
+
43
+ # Calls the controller to verify the authenticity token.
44
+ def rails_check_csrf!
45
+ rails_controller_instance.send(:verify_authenticity_token)
46
+ end
47
+
48
+ # Hidden tag with Rails CSRF token inserted into Rodauth templates.
49
+ def rails_csrf_tag
50
+ %(<input type="hidden" name="#{rails_csrf_param}" value="#{rails_csrf_token}">)
51
+ end
52
+
53
+ # The request parameter under which to send the Rails CSRF token.
54
+ def rails_csrf_param
55
+ rails_controller.request_forgery_protection_token
56
+ end
57
+
58
+ # The Rails CSRF token value inserted into Rodauth templates.
59
+ def rails_csrf_token
60
+ rails_controller_instance.send(:form_authenticity_token)
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,30 @@
1
+ module Rodauth
2
+ module Rails
3
+ module Feature
4
+ module Email
5
+ def self.included(feature)
6
+ feature.depends :email_base
7
+ end
8
+
9
+ private
10
+
11
+ # Create emails with ActionMailer which uses configured delivery method.
12
+ def create_email_to(to, subject, body)
13
+ Mailer.create_email(to: to, from: email_from, subject: "#{email_subject_prefix}#{subject}", body: body)
14
+ end
15
+
16
+ # Delivers the given email.
17
+ def send_email(email)
18
+ email.deliver_now
19
+ end
20
+
21
+ # ActionMailer subclass for correct email delivering.
22
+ class Mailer < ActionMailer::Base
23
+ def create_email(**options)
24
+ mail(**options)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,71 @@
1
+ module Rodauth
2
+ module Rails
3
+ module Feature
4
+ module Instrumentation
5
+ private
6
+
7
+ def _around_rodauth
8
+ rails_instrument_request { super }
9
+ end
10
+
11
+ def redirect(*)
12
+ rails_instrument_redirection { super }
13
+ end
14
+
15
+ def rails_render(*)
16
+ render_output = nil
17
+ rails_controller_instance.view_runtime = rails_controller_instance.send(:cleanup_view_runtime) do
18
+ Benchmark.ms { render_output = super }
19
+ end
20
+ render_output
21
+ end
22
+
23
+ def rails_instrument_request
24
+ request = rails_request
25
+
26
+ raw_payload = {
27
+ controller: scope.class.superclass.name,
28
+ action: "call",
29
+ request: request,
30
+ params: request.filtered_parameters,
31
+ headers: request.headers,
32
+ format: request.format.ref,
33
+ method: request.request_method,
34
+ path: request.fullpath
35
+ }
36
+
37
+ ActiveSupport::Notifications.instrument("start_processing.action_controller", raw_payload)
38
+
39
+ ActiveSupport::Notifications.instrument("process_action.action_controller", raw_payload) do |payload|
40
+ begin
41
+ result = catch(:halt) { yield }
42
+
43
+ response = ActionDispatch::Response.new *(result || [404, {}, []])
44
+ payload[:response] = response
45
+ payload[:status] = response.status
46
+
47
+ throw :halt, result if result
48
+ rescue => error
49
+ payload[:status] = ActionDispatch::ExceptionWrapper.status_code_for_exception(error.class.name)
50
+ raise
51
+ ensure
52
+ rails_controller_eval { append_info_to_payload(payload) }
53
+ end
54
+ end
55
+ end
56
+
57
+ def rails_instrument_redirection
58
+ ActiveSupport::Notifications.instrument("redirect_to.action_controller", request: rails_request) do |payload|
59
+ result = catch(:halt) { yield }
60
+
61
+ response = ActionDispatch::Response.new(*result)
62
+ payload[:status] = response.status
63
+ payload[:location] = response.filtered_location
64
+
65
+ throw :halt, result
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,41 @@
1
+ module Rodauth
2
+ module Rails
3
+ module Feature
4
+ module Render
5
+ def self.included(feature)
6
+ feature.auth_methods :rails_render
7
+ end
8
+
9
+ # Renders templates with layout. First tries to render a user-defined
10
+ # template, otherwise falls back to Rodauth's template.
11
+ def view(page, *)
12
+ rails_render(action: page.tr("-", "_"), layout: true) ||
13
+ rails_render(html: super.html_safe, layout: true)
14
+ end
15
+
16
+ # Renders templates without layout. First tries to render a user-defined
17
+ # template or partial, otherwise falls back to Rodauth's template.
18
+ def render(page)
19
+ rails_render(partial: page.tr("-", "_"), layout: false) ||
20
+ rails_render(action: page.tr("-", "_"), layout: false) ||
21
+ super.html_safe
22
+ end
23
+
24
+ def button(*)
25
+ super.html_safe
26
+ end
27
+
28
+ private
29
+
30
+ # Calls the Rails renderer, returning nil if a template is missing.
31
+ def rails_render(*args)
32
+ return if rails_api_controller?
33
+
34
+ rails_controller_instance.render_to_string(*args)
35
+ rescue ActionView::MissingTemplate
36
+ nil
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,6 +1,5 @@
1
1
  require "rodauth/rails/middleware"
2
2
  require "rodauth/rails/controller_methods"
3
- require "rodauth/rails/log_subscriber"
4
3
 
5
4
  require "rails"
6
5
 
@@ -17,10 +16,6 @@ module Rodauth
17
16
  end
18
17
  end
19
18
 
20
- initializer "rodauth.log_subscriber" do
21
- Rodauth::Rails::LogSubscriber.attach_to :rodauth
22
- end
23
-
24
19
  initializer "rodauth.test" do
25
20
  # Rodauth uses RACK_ENV to set the default bcrypt hash cost
26
21
  ENV["RACK_ENV"] = "test" if ::Rails.env.test?
@@ -1,5 +1,5 @@
1
1
  module Rodauth
2
2
  module Rails
3
- VERSION = "0.11.0"
3
+ VERSION = "0.12.0"
4
4
  end
5
5
  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.11.0
4
+ version: 0.12.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-05-06 00:00:00.000000000 Z
11
+ date: 2021-05-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: railties
@@ -207,7 +207,12 @@ files:
207
207
  - lib/rodauth/rails/auth.rb
208
208
  - lib/rodauth/rails/controller_methods.rb
209
209
  - lib/rodauth/rails/feature.rb
210
- - lib/rodauth/rails/log_subscriber.rb
210
+ - lib/rodauth/rails/feature/base.rb
211
+ - lib/rodauth/rails/feature/callbacks.rb
212
+ - lib/rodauth/rails/feature/csrf.rb
213
+ - lib/rodauth/rails/feature/email.rb
214
+ - lib/rodauth/rails/feature/instrumentation.rb
215
+ - lib/rodauth/rails/feature/render.rb
211
216
  - lib/rodauth/rails/middleware.rb
212
217
  - lib/rodauth/rails/railtie.rb
213
218
  - lib/rodauth/rails/tasks.rake
@@ -1,34 +0,0 @@
1
- module Rodauth
2
- module Rails
3
- class LogSubscriber < ActiveSupport::LogSubscriber
4
- def start_processing(event)
5
- rodauth = event.payload[:rodauth]
6
- app_class = rodauth.scope.class.superclass
7
- format = rodauth.rails_request.format.ref
8
- format = format.to_s.upcase if format.is_a?(Symbol)
9
- format = "*/*" if format.nil?
10
-
11
- info "Processing by #{app_class} as #{format}"
12
- end
13
-
14
- def process_request(event)
15
- status = event.payload[:status]
16
-
17
- additions = ActionController::Base.log_process_action(event.payload)
18
- if ::Rails.gem_version >= Gem::Version.new("6.0")
19
- additions << "Allocations: #{event.allocations}"
20
- end
21
-
22
- message = "Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in #{event.duration.round}ms"
23
- message << " (#{additions.join(" | ")})"
24
- message << "\n\n" if defined?(::Rails.env) && ::Rails.env.development?
25
-
26
- info message
27
- end
28
-
29
- def logger
30
- ::Rails.logger
31
- end
32
- end
33
- end
34
- end