rodauth-rails 0.11.0 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
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