rodauth-rails 0.1.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.
@@ -0,0 +1,38 @@
1
+ require "rails/generators/base"
2
+ require "rodauth/version"
3
+
4
+ module Rodauth
5
+ module Rails
6
+ module Generators
7
+ class MailerGenerator < ::Rails::Generators::Base
8
+ source_root "#{__dir__}/templates"
9
+ namespace "rodauth:mailer"
10
+
11
+ VIEWS = %w[
12
+ email_auth
13
+ password_changed
14
+ reset_password
15
+ unlock_account
16
+ verify_account
17
+ verify_login_change
18
+ ]
19
+
20
+ class_option :name,
21
+ desc: "The name for the mailer and the views directory",
22
+ default: "rodauth"
23
+
24
+ def copy_mailer
25
+ template "app/mailers/rodauth_mailer.rb",
26
+ "app/mailers/#{options[:name].underscore}_mailer.rb"
27
+ end
28
+
29
+ def copy_mailer_views
30
+ VIEWS.each do |view|
31
+ template "app/views/rodauth_mailer/#{view}.text.erb",
32
+ "app/views/#{options[:name].underscore}_mailer/#{view}.text.erb"
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,3 @@
1
+ class RodauthController < ApplicationController
2
+ # used by Rodauth for rendering views and CSRF protection
3
+ end
@@ -0,0 +1,37 @@
1
+ class <%= options[:name].camelize %>Mailer < ApplicationMailer
2
+ def verify_account(recipient, email_link)
3
+ @email_link = email_link
4
+
5
+ mail to: recipient
6
+ end
7
+
8
+ def reset_password(recipient, email_link)
9
+ @email_link = email_link
10
+
11
+ mail to: recipient
12
+ end
13
+
14
+ def verify_login_change(recipient, old_login, new_login, email_link)
15
+ @old_login = old_login
16
+ @new_login = new_login
17
+ @email_link = email_link
18
+
19
+ mail to: recipient
20
+ end
21
+
22
+ def password_changed(recipient)
23
+ mail to: recipient
24
+ end
25
+
26
+ # def email_auth(recipient, email_link)
27
+ # @email_link = email_link
28
+
29
+ # mail to: recipient
30
+ # end
31
+
32
+ # def unlock_account(recipient, email_link)
33
+ # @email_link = email_link
34
+
35
+ # mail to: recipient
36
+ # end
37
+ end
@@ -0,0 +1,2 @@
1
+ class Account < ApplicationRecord
2
+ end
@@ -0,0 +1,3 @@
1
+ Rodauth::Rails.configure do |config|
2
+ config.app = "RodauthApp"
3
+ end
@@ -0,0 +1,13 @@
1
+ require "sequel/core"
2
+
3
+ # initialize the appropriate Sequel adapter without creating a connection
4
+ <% case adapter -%>
5
+ <% when "postgresql" -%>
6
+ DB = Sequel.postgres(test: false)
7
+ <% when "mysql2" -%>
8
+ DB = Sequel.mysql2(test: false)
9
+ <% when "sqlite3" -%>
10
+ DB = Sequel.sqlite(test: false)
11
+ <% end -%>
12
+ # have Sequel use ActiveRecord's connection for database interaction
13
+ DB.extension :activerecord_connection
@@ -0,0 +1,170 @@
1
+ class CreateRodauth < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ <% if adapter == "postgresql" -%>
4
+ enable_extension "citext"
5
+
6
+ <% end -%>
7
+ create_table :accounts do |t|
8
+ <% case adapter -%>
9
+ <% when "postgresql" -%>
10
+ t.citext :email, null: false, index: { unique: true, where: "status IN ('verified', 'unverified')" }
11
+ <% else -%>
12
+ t.string :email, null: false, index: { unique: true }
13
+ <% end -%>
14
+ t.string :status, null: false, default: "verified"
15
+ end
16
+
17
+ # Used if storing password hashes in a separate table (default)
18
+ create_table :account_password_hashes do |t|
19
+ t.foreign_key :accounts, column: :id
20
+ t.string :password_hash, null: false
21
+ end
22
+
23
+ # Used by the password reset feature
24
+ create_table :account_password_reset_keys do |t|
25
+ t.foreign_key :accounts, column: :id
26
+ t.string :key, null: false
27
+ t.datetime :deadline, null: false
28
+ t.datetime :email_last_sent, null: false, default: -> { "CURRENT_TIMESTAMP" }
29
+ end
30
+
31
+ # Used by the account verification feature
32
+ create_table :account_verification_keys do |t|
33
+ t.foreign_key :accounts, column: :id
34
+ t.string :key, null: false
35
+ t.datetime :requested_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
36
+ t.datetime :email_last_sent, null: false, default: -> { "CURRENT_TIMESTAMP" }
37
+ end
38
+
39
+ # Used by the verify login change feature
40
+ create_table :account_login_change_keys do |t|
41
+ t.foreign_key :accounts, column: :id
42
+ t.string :key, null: false
43
+ t.string :login, null: false
44
+ t.datetime :deadline, null: false
45
+ end
46
+
47
+ # Used by the remember me feature
48
+ create_table :account_remember_keys do |t|
49
+ t.foreign_key :accounts, column: :id
50
+ t.string :key, null: false
51
+ t.datetime :deadline, null: false
52
+ end
53
+
54
+ # # Used by the audit logging feature
55
+ # create_table :account_authentication_audit_logs do |t|
56
+ # t.references :account, null: false
57
+ # t.datetime :at, null: false, default: -> { "CURRENT_TIMESTAMP" }
58
+ # t.text :message, null: false
59
+ <% case adapter -%>
60
+ <% when "postgresql" -%>
61
+ # t.jsonb :metadata
62
+ <% when "sqlite3", "mysql2" -%>
63
+ # t.json :metadata
64
+ <% else -%>
65
+ # t.string :metadata
66
+ <% end -%>
67
+ # t.index [:account_id, :at], name: "audit_account_at_idx"
68
+ # t.index :at, name: "audit_at_idx"
69
+ # end
70
+
71
+ # # Used by the jwt refresh feature
72
+ # create_table :account_jwt_refresh_keys do |t|
73
+ # t.references :account, null: false
74
+ # t.string :key, null: false
75
+ # t.datetime :deadline, null: false
76
+ # t.index :account_id, name: "account_jwt_rk_account_id_idx"
77
+ # end
78
+
79
+ # # Used by the disallow_password_reuse feature
80
+ # create_table :account_previous_password_hashes do |t|
81
+ # t.references :account
82
+ # t.string :password_hash, null: false
83
+ # end
84
+
85
+ # # Used by the lockout feature
86
+ # create_table :account_login_failures do |t|
87
+ # t.foreign_key :accounts, column: :id
88
+ # t.integer :number, null: false, default: 1
89
+ # end
90
+ # create_table :account_lockouts do |t|
91
+ # t.foreign_key :accounts, column: :id
92
+ # t.string :key, null: false
93
+ # t.datetime :deadline, null: false
94
+ # t.datetime :email_last_sent
95
+ # end
96
+
97
+ # # Used by the email auth feature
98
+ # create_table :account_email_auth_keys do |t|
99
+ # t.foreign_key :accounts, column: :id
100
+ # t.string :key, null: false
101
+ # t.datetime :deadline, null: false
102
+ # t.datetime :email_last_sent, null: false, default: -> { "CURRENT_TIMESTAMP" }
103
+ # end
104
+
105
+ # # Used by the password expiration feature
106
+ # create_table :account_password_change_times do |t|
107
+ # t.foreign_key :accounts, column: :id
108
+ # t.datetime :changed_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
109
+ # end
110
+
111
+ # # Used by the account expiration feature
112
+ # create_table :account_activity_times do |t|
113
+ # t.foreign_key :accounts, column: :id
114
+ # t.datetime :last_activity_at, null: false
115
+ # t.datetime :last_login_at, null: false
116
+ # t.datetime :expired_at
117
+ # end
118
+
119
+ # # Used by the single session feature
120
+ # create_table :account_session_keys do |t|
121
+ # t.foreign_key :accounts, column: :id
122
+ # t.string :key, null: false
123
+ # end
124
+
125
+ # # Used by the active sessions feature
126
+ # create_table :account_active_session_keys, primary_key: [:account_id, :session_id] do |t|
127
+ # t.references :account
128
+ # t.string :session_id
129
+ # t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
130
+ # t.datetime :last_use, null: false, default: -> { "CURRENT_TIMESTAMP" }
131
+ # end
132
+
133
+ # # Used by the webauthn feature
134
+ # create_table :account_webauthn_user_ids do |t|
135
+ # t.foreign_key :accounts, column: :id
136
+ # t.string :webauthn_id, null: false
137
+ # end
138
+ # create_table :account_webauthn_keys, primary_key: [:account_id, :webauthn_id] do |t|
139
+ # t.references :account
140
+ # t.string :webauthn_id
141
+ # t.string :public_key, null: false
142
+ # t.integer :sign_count, null: false
143
+ # t.datetime :last_use, null: false, default: -> { "CURRENT_TIMESTAMP" }
144
+ # end
145
+
146
+ # # Used by the otp feature
147
+ # create_table :account_otp_keys do |t|
148
+ # t.foreign_key :accounts, column: :id
149
+ # t.string :key, null: false
150
+ # t.integer :num_failures, null: false, default: 0
151
+ # t.datetime :last_use, null: false, default: -> { "CURRENT_TIMESTAMP" }
152
+ # end
153
+
154
+ # # Used by the recovery codes feature
155
+ # create_table :account_recovery_codes, primary_key: [:id, :code] do |t|
156
+ # t.integer :id
157
+ # t.foreign_key :accounts, column: :id
158
+ # t.string :code
159
+ # end
160
+
161
+ # # Used by the sms codes feature
162
+ # create_table :account_sms_codes do |t|
163
+ # t.foreign_key :accounts, column: :id
164
+ # t.string :phone_number, null: false
165
+ # t.integer :num_failures
166
+ # t.string :code
167
+ # t.datetime :code_issued_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
168
+ # end
169
+ end
170
+ end
@@ -0,0 +1,186 @@
1
+ class RodauthApp < Rodauth::Rails::App
2
+ configure do
3
+ # List of authentication features that are loaded.
4
+ enable :create_account, :verify_account, :verify_account_grace_period,
5
+ :login, :remember, :logout,
6
+ :reset_password, :change_password, :change_password_notify,
7
+ :change_login, :verify_login_change,
8
+ :close_account
9
+
10
+ # See the Rodauth documentation for the list of available config options:
11
+ # http://rodauth.jeremyevans.net/documentation.html
12
+
13
+ # ==> General
14
+ # Specify the controller used for view rendering and CSRF verification.
15
+ rails_controller { RodauthController }
16
+
17
+ # Store account status in a text column.
18
+ account_status_column :status
19
+ account_unverified_status_value "unverified"
20
+ account_open_status_value "verified"
21
+ account_closed_status_value "closed"
22
+
23
+ # Store password hash in a column instead of a separate table.
24
+ # account_password_hash_column :password_digest
25
+
26
+ # Set password when creating account instead of when verifying.
27
+ verify_account_set_password? false
28
+
29
+ # Redirect back to originally requested location after authentication.
30
+ # login_return_to_requested_location? true
31
+ # two_factor_auth_return_to_requested_location? true # if using MFA
32
+
33
+ # Autologin the user after they have reset their password.
34
+ # reset_password_autologin? true
35
+
36
+ # Delete the account record when the user has closed their account.
37
+ # delete_account_on_close? true
38
+
39
+ # Redirect to the app from login and registration pages if already logged in.
40
+ # already_logged_in { redirect login_redirect }
41
+
42
+ # ==> Emails
43
+ # Uncomment the lines below once you've imported mailer views.
44
+ # send_reset_password_email do
45
+ # RodauthMailer.reset_password(email_to, password_reset_email_link).deliver_now
46
+ # end
47
+ # send_verify_account_email do
48
+ # RodauthMailer.verify_account(email_to, verify_account_email_link).deliver_now
49
+ # end
50
+ # send_verify_login_change_email do |login|
51
+ # RodauthMailer.verify_login_change(login, verify_login_change_old_login, verify_login_change_new_login, verify_login_change_email_link).deliver_now
52
+ # end
53
+ # send_password_changed_email do
54
+ # RodauthMailer.password_changed(email_to).deliver_now
55
+ # end
56
+ # # send_email_auth_email do
57
+ # # RodauthMailer.email_auth(email_to, email_auth_email_link).deliver_now
58
+ # # end
59
+ # # send_unlock_account_email do
60
+ <% if Rodauth::MAJOR == 1 -%>
61
+ # # @unlock_account_key_value = get_unlock_account_key
62
+ <% end -%>
63
+ # # RodauthMailer.unlock_account(email_to, unlock_account_email_link).deliver_now
64
+ # # end
65
+
66
+ # In the meantime you can tweak settings for emails created by Rodauth
67
+ # email_subject_prefix "[MyApp] "
68
+ # email_from "noreply@myapp.com"
69
+ # send_email(&:deliver_later)
70
+ # reset_password_email_body { "Click here to reset your password: #{reset_password_email_link}" }
71
+
72
+ # ==> Flash
73
+ # Match flash keys with ones already used in the Rails app.
74
+ # flash_notice_key :success # default is :notice
75
+ # flash_error_key :error # default is :alert
76
+
77
+ # Override default flash messages.
78
+ # create_account_notice_flash "Your account has been created. Please verify your account by visiting the confirmation link sent to your email address."
79
+ # login_error_flash "Login is required for accessing this page"
80
+ # login_notice_flash nil
81
+
82
+ # ==> Validation
83
+ # Override default validation error messages.
84
+ # no_matching_login_message "user with this email address doesn't exist"
85
+ # already_an_account_with_this_login_message "user with this email address already exists"
86
+ # password_too_short_message { "needs to have at least #{password_minimum_length} characters" }
87
+ # login_does_not_meet_requirements_message { "invalid email#{", #{login_requirement_message}" if login_requirement_message}" }
88
+
89
+ # Change minimum number of password characters required when creating an account.
90
+ # password_minimum_length 8
91
+
92
+ # ==> Remember Feature
93
+ # Remember all logged in users.
94
+ after_login { remember_login }
95
+
96
+ # Or only remember users that have ticked a "Remember Me" checkbox on login.
97
+ # after_login { remember_login if param_or_nil("remember") }
98
+
99
+ # Extend user's remember period when remembered via a cookie
100
+ extend_remember_deadline? true
101
+
102
+ # Consider remembered users to be multifactor-authenticated (if using MFA).
103
+ # after_load_memory { two_factor_update_session("totp") if two_factor_authentication_setup? }
104
+
105
+ # ==> Hooks
106
+ # Validate custom fields in the create account form.
107
+ # before_create_account do
108
+ # throw_error_status(422, "name", "must be present") if param("name").empty?
109
+ # end
110
+
111
+ # Perform additional actions after the account is created.
112
+ # after_create_account do
113
+ # Profile.create!(account_id: account[:id], name: param("name"))
114
+ # end
115
+
116
+ # Do additional cleanup after the account is closed.
117
+ # after_close_account do
118
+ # Profile.find_by!(account_id: account[:id]).destroy
119
+ # end
120
+
121
+ # ==> Redirects
122
+ # Redirect to home page after logout.
123
+ logout_redirect "/"
124
+
125
+ # Redirect to wherever login redirects to after account verification.
126
+ verify_account_redirect { login_redirect }
127
+
128
+ # Redirect to login page after password reset.
129
+ reset_password_redirect { login_path }
130
+
131
+ # ==> Deadlines
132
+ # Change default deadlines for some actions.
133
+ # verify_account_grace_period 3.days
134
+ # reset_password_deadline_interval Hash[hours: 6]
135
+ # verify_login_change_deadline_interval Hash[days: 2]
136
+ # remember_deadline_interval Hash[days: 30]
137
+
138
+ # ==> Extending
139
+ # Define any additional methods you want for the Rodauth object.
140
+ # auth_class_eval do
141
+ # def my_send_email(name, *args)
142
+ # AuthenticationMailer.public_send(name, *args).deliver_later
143
+ # end
144
+ # end
145
+ #
146
+ # Then use the new custom method in configuration blocks.
147
+ # send_password_reset_email do
148
+ # my_send_email(:password_reset, email_to, password_reset_email_link)
149
+ # end
150
+ end
151
+
152
+ # ==> Multiple configurations
153
+ # configure(:admin) do
154
+ # enable :http_basic_auth
155
+ #
156
+ # prefix "/admin"
157
+ # session_key :admin_id
158
+ # end
159
+
160
+ route do |r|
161
+ rodauth.load_memory # autologin remembered users
162
+
163
+ r.rodauth # route rodauth requests
164
+
165
+ # ==> Authenticating Requests
166
+ # Call `rodauth.require_authentication` for requests that you want to
167
+ # require authentication for. Some examples:
168
+ #
169
+ # next if r.path.start_with?("/docs") # skip authentication for documentation pages
170
+ # next if session[:admin] # skip authentication for admins
171
+ #
172
+ # # authenticate /dashboard/* and /account/* requests
173
+ # if r.path.start_with?("/dashboard") || r.path.start_with?("/account")
174
+ # rodauth.require_authentication
175
+ # end
176
+
177
+ # ==> Multiple configurations
178
+ # r.on "admin" do
179
+ # r.rodauth(:admin)
180
+ #
181
+ # unless rodauth(:admin).logged_in?
182
+ # rodauth(:admin).require_http_basic_auth
183
+ # end
184
+ # end
185
+ end
186
+ end
@@ -0,0 +1,123 @@
1
+ require "rails/generators/base"
2
+ require "rodauth/version"
3
+
4
+ module Rodauth
5
+ module Rails
6
+ module Generators
7
+ class ViewsGenerator < ::Rails::Generators::Base
8
+ source_root "#{__dir__}/templates"
9
+ namespace "rodauth:views"
10
+
11
+ VIEWS = {
12
+ login: %w[
13
+ _field _field_error _login_field _login_display _password_field
14
+ _submit _login_form _login_form_footer _login_form_header login
15
+ multi_phase_login
16
+ ],
17
+ create_account: %w[
18
+ _field _field_error _login_field _login_confirm_field
19
+ _password_field _password_confirm_field _submit create_account
20
+ ],
21
+ logout: %w[
22
+ _submit logout
23
+ ],
24
+ reset_password: %w[
25
+ _field _field_error _login_field _login_hidden_field
26
+ _password_field _password_confirm_field _submit
27
+ reset_password_request reset_password
28
+ ],
29
+ remember: %w[
30
+ _submit remember
31
+ ],
32
+ change_login: %w[
33
+ _field _field_error _login_field _login_confirm_field
34
+ _password_field _submit change_login
35
+ ],
36
+ change_password: %w[
37
+ _field _field_error _password_field _new_password_field
38
+ _password_confirm_field _submit change_password
39
+ ],
40
+ close_account: %w[
41
+ _field _field_error _password_field _submit close_account
42
+ ],
43
+ email_auth: %w[
44
+ _login_hidden_field _submit _email_auth_request_form email_auth
45
+ ],
46
+ verify_account: %w[
47
+ _field _field_error _login_hidden_field _login_field _submit
48
+ verify_account_resend verify_account
49
+ ],
50
+ lockout: %w[
51
+ _login_hidden_field _submit unlock_account_request unlock_account
52
+ ],
53
+ active_sessions: %w[
54
+ _global_logout_field
55
+ ],
56
+ two_factor_base: %w[
57
+ _field _field_error _password_field _submit
58
+ two_factor_manage two_factor_auth two_factor_disable
59
+ ],
60
+ otp: %w[
61
+ _field _field_error _otp_auth_code_field _password_field _submit
62
+ otp_setup otp_auth otp_disable
63
+ ],
64
+ sms_codes: %w[
65
+ _field _field_error _sms_code_field _sms_phone_field
66
+ _password_field _submit
67
+ sms_setup sms_confirm sms_auth sms_request sms_disable
68
+ ],
69
+ recovery_codes: %w[
70
+ _field _field_error _recovery_code_field _recovery_codes_form
71
+ recovery_codes add_recovery_codes recovery_auth
72
+ ],
73
+ webauthn: %w[
74
+ _field _field_error _login_hidden_field _password_field _submit
75
+ webauthn_setup webauthn_auth webauthn_remove
76
+ ]
77
+ }
78
+
79
+ DEPENDENCIES = {
80
+ active_sessions: :logout,
81
+ otp: :two_factor_base,
82
+ sms_codes: :two_factor_base,
83
+ recovery_codes: :two_factor_base,
84
+ webauthn: :two_factor_base,
85
+ }
86
+
87
+ class_option :features, type: :array,
88
+ desc: "Rodauth features to generate views for (login, create_account, reset_password, verify_account etc.)",
89
+ default: %w[login logout create_account verify_account reset_password change_password change_login verify_login_change close_account]
90
+
91
+ class_option :all, aliases: "-a", type: :boolean,
92
+ desc: "Generates views for all Rodauth features",
93
+ default: false
94
+
95
+ class_option :directory, aliases: "-d", type: :string,
96
+ desc: "The directory under app/views/* into which to create views",
97
+ default: "rodauth"
98
+
99
+ def create_views
100
+ features = options[:all] ? VIEWS.keys : options[:features].map(&:to_sym)
101
+
102
+ views = features.inject([]) do |list, feature|
103
+ list |= VIEWS[feature] || []
104
+ list |= VIEWS[DEPENDENCIES[feature]] || []
105
+ end
106
+
107
+ if Rodauth::MAJOR == 1
108
+ views -= %w[
109
+ multi_phase_login _global_logout_field
110
+ two_factor_manage two_factor_auth two_factor_disable
111
+ webauthn_setup webauthn_auth webauthn_remove
112
+ ]
113
+ end
114
+
115
+ views.each do |view|
116
+ template "app/views/rodauth/#{view}.html.erb",
117
+ "app/views/#{options[:directory].underscore}/#{view}.html.erb"
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1 @@
1
+ require "rodauth/rails/feature"
@@ -0,0 +1,50 @@
1
+ module Rodauth
2
+ module Rails
3
+ class App
4
+ # Sets up Rails' flash integration.
5
+ module Flash
6
+ def self.load_dependencies(app)
7
+ app.plugin :hooks
8
+ end
9
+
10
+ def self.configure(app)
11
+ app.before { request.flash } # load flash
12
+ app.after { request.commit_flash } # save flash
13
+ end
14
+
15
+ module InstanceMethods
16
+ def flash
17
+ request.flash
18
+ end
19
+ end
20
+
21
+ module RequestMethods
22
+ # If the redirect would bubble up outside of the Roda app, the after
23
+ # hook would never get called, so we make sure to commit the flash.
24
+ def redirect(*)
25
+ commit_flash
26
+ super
27
+ end
28
+
29
+ def flash
30
+ rails_request.flash
31
+ end
32
+
33
+ def commit_flash
34
+ if ActionPack.version >= Gem::Version.new("5.0.0")
35
+ rails_request.commit_flash
36
+ else
37
+ # ActionPack 4.2 automatically commits flash
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def rails_request
44
+ ActionDispatch::Request.new(env)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,45 @@
1
+ require "roda"
2
+
3
+ module Rodauth
4
+ module Rails
5
+ # The superclass for creating a Rodauth middleware.
6
+ class App < Roda
7
+ require "rodauth/rails/app/flash"
8
+
9
+ plugin :middleware
10
+ plugin :hooks
11
+ plugin :render, layout: false
12
+
13
+ plugin Flash
14
+
15
+ def self.configure(name = nil, **options, &block)
16
+ plugin :rodauth, name: name, csrf: false, flash: false, **options do
17
+ # load the Rails integration
18
+ enable :rails
19
+
20
+ # database functions are more complex to set up, so disable them by default
21
+ use_database_authentication_functions? false
22
+
23
+ # avoid having to set deadline values in column default values
24
+ set_deadline_values? true
25
+
26
+ # use HMACs for additional security
27
+ hmac_secret { ::Rails.application.secrets.secret_key_base }
28
+
29
+ # evaluate user configuration
30
+ instance_exec(&block)
31
+ end
32
+ end
33
+
34
+ before do
35
+ (opts[:rodauths] || {}).each do |name, _|
36
+ if name
37
+ env["rodauth.#{name}"] = rodauth(name)
38
+ else
39
+ env["rodauth"] = rodauth
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end