rodauth-rails 1.7.0 → 1.8.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -0
  3. data/LICENSE.txt +1 -1
  4. data/README.md +50 -10
  5. data/lib/generators/rodauth/install_generator.rb +35 -34
  6. data/lib/generators/rodauth/migration/active_record/account_expiration.erb +2 -2
  7. data/lib/generators/rodauth/migration/active_record/active_sessions.erb +2 -2
  8. data/lib/generators/rodauth/migration/active_record/audit_logging.erb +3 -3
  9. data/lib/generators/rodauth/migration/active_record/base.erb +1 -1
  10. data/lib/generators/rodauth/migration/active_record/disallow_password_reuse.erb +2 -2
  11. data/lib/generators/rodauth/migration/active_record/email_auth.erb +2 -1
  12. data/lib/generators/rodauth/migration/active_record/jwt_refresh.erb +3 -3
  13. data/lib/generators/rodauth/migration/active_record/lockout.erb +4 -4
  14. data/lib/generators/rodauth/migration/active_record/otp.erb +2 -2
  15. data/lib/generators/rodauth/migration/active_record/password_expiration.erb +2 -2
  16. data/lib/generators/rodauth/migration/active_record/recovery_codes.erb +2 -2
  17. data/lib/generators/rodauth/migration/active_record/remember.erb +2 -2
  18. data/lib/generators/rodauth/migration/active_record/reset_password.erb +2 -2
  19. data/lib/generators/rodauth/migration/active_record/single_session.erb +2 -2
  20. data/lib/generators/rodauth/migration/active_record/sms_codes.erb +2 -2
  21. data/lib/generators/rodauth/migration/active_record/verify_account.erb +2 -2
  22. data/lib/generators/rodauth/migration/active_record/verify_login_change.erb +2 -2
  23. data/lib/generators/rodauth/migration/active_record/webauthn.erb +4 -4
  24. data/lib/generators/rodauth/migration/sequel/account_expiration.erb +2 -2
  25. data/lib/generators/rodauth/migration/sequel/active_sessions.erb +3 -3
  26. data/lib/generators/rodauth/migration/sequel/audit_logging.erb +3 -3
  27. data/lib/generators/rodauth/migration/sequel/base.erb +1 -1
  28. data/lib/generators/rodauth/migration/sequel/disallow_password_reuse.erb +2 -2
  29. data/lib/generators/rodauth/migration/sequel/email_auth.erb +2 -2
  30. data/lib/generators/rodauth/migration/sequel/jwt_refresh.erb +3 -3
  31. data/lib/generators/rodauth/migration/sequel/lockout.erb +4 -4
  32. data/lib/generators/rodauth/migration/sequel/otp.erb +2 -2
  33. data/lib/generators/rodauth/migration/sequel/password_expiration.erb +2 -2
  34. data/lib/generators/rodauth/migration/sequel/recovery_codes.erb +2 -2
  35. data/lib/generators/rodauth/migration/sequel/remember.erb +2 -2
  36. data/lib/generators/rodauth/migration/sequel/reset_password.erb +2 -2
  37. data/lib/generators/rodauth/migration/sequel/single_session.erb +2 -2
  38. data/lib/generators/rodauth/migration/sequel/sms_codes.erb +2 -2
  39. data/lib/generators/rodauth/migration/sequel/verify_account.erb +2 -2
  40. data/lib/generators/rodauth/migration/sequel/verify_login_change.erb +2 -2
  41. data/lib/generators/rodauth/migration/sequel/webauthn.erb +5 -5
  42. data/lib/generators/rodauth/migration_generator.rb +43 -1
  43. data/lib/generators/rodauth/templates/app/mailers/{rodauth_mailer.rb → rodauth_mailer.rb.tt} +8 -6
  44. data/lib/generators/rodauth/templates/app/misc/{rodauth_main.rb → rodauth_main.rb.tt} +69 -10
  45. data/lib/generators/rodauth/templates/app/models/{account.rb → account.rb.tt} +2 -2
  46. data/lib/generators/rodauth/templates/app/views/rodauth/tailwind/webauthn_auth.html.erb +13 -0
  47. data/lib/generators/rodauth/templates/app/views/rodauth/tailwind/webauthn_remove.html.erb +21 -0
  48. data/lib/generators/rodauth/templates/app/views/rodauth/tailwind/webauthn_setup.html.erb +21 -0
  49. data/lib/generators/rodauth/templates/app/views/rodauth/webauthn_auth.html.erb +3 -3
  50. data/lib/generators/rodauth/templates/app/views/rodauth/webauthn_setup.html.erb +4 -4
  51. data/lib/generators/rodauth/templates/test/fixtures/{accounts.yml → accounts.yml.tt} +2 -2
  52. data/lib/rodauth/rails/app.rb +19 -0
  53. data/lib/rodauth/rails/feature/base.rb +0 -10
  54. data/lib/rodauth/rails/feature/email.rb +2 -2
  55. data/lib/rodauth/rails/feature/internal_request.rb +1 -1
  56. data/lib/rodauth/rails/version.rb +1 -1
  57. data/rodauth-rails.gemspec +1 -1
  58. metadata +16 -14
  59. data/lib/generators/rodauth/templates/config/initializers/sequel.rb +0 -4
  60. /data/lib/generators/rodauth/templates/app/controllers/{rodauth_controller.rb → rodauth_controller.rb.tt} +0 -0
  61. /data/lib/generators/rodauth/templates/app/misc/{rodauth_app.rb → rodauth_app.rb.tt} +0 -0
  62. /data/lib/generators/rodauth/templates/config/initializers/{rodauth.rb → rodauth.rb.tt} +0 -0
  63. /data/lib/generators/rodauth/templates/db/migrate/{create_rodauth.rb → create_rodauth.rb.tt} +0 -0
@@ -1,6 +1,6 @@
1
1
  # Used by the remember me feature
2
- create_table :account_remember_keys do
3
- foreign_key :id, :accounts, primary_key: true, type: :Bignum
2
+ create_table :<%= table_prefix %>_remember_keys do
3
+ foreign_key :id, :<%= table_prefix.pluralize %>, primary_key: true, type: :Bignum
4
4
  String :key, null: false
5
5
  DateTime :deadline, null: false
6
6
  end
@@ -1,6 +1,6 @@
1
1
  # Used by the password reset feature
2
- create_table :account_password_reset_keys do
3
- foreign_key :id, :accounts, primary_key: true, type: :Bignum
2
+ create_table :<%= table_prefix %>_password_reset_keys do
3
+ foreign_key :id, :<%= table_prefix.pluralize %>, primary_key: true, type: :Bignum
4
4
  String :key, null: false
5
5
  DateTime :deadline, null: false
6
6
  DateTime :email_last_sent, null: false, default: Sequel::CURRENT_TIMESTAMP
@@ -1,5 +1,5 @@
1
1
  # Used by the single session feature
2
- create_table :account_session_keys do
3
- foreign_key :id, :accounts, primary_key: true, type: :Bignum
2
+ create_table :<%= table_prefix %>_session_keys do
3
+ foreign_key :id, :<%= table_prefix.pluralize %>, primary_key: true, type: :Bignum
4
4
  String :key, null: false
5
5
  end
@@ -1,6 +1,6 @@
1
1
  # Used by the sms codes feature
2
- create_table :account_sms_codes do
3
- foreign_key :id, :accounts, primary_key: true, type: :Bignum
2
+ create_table :<%= table_prefix %>_sms_codes do
3
+ foreign_key :id, :<%= table_prefix.pluralize %>, primary_key: true, type: :Bignum
4
4
  String :phone_number, null: false
5
5
  Integer :num_failures
6
6
  String :code
@@ -1,6 +1,6 @@
1
1
  # Used by the account verification feature
2
- create_table :account_verification_keys do
3
- foreign_key :id, :accounts, primary_key: true, type: :Bignum
2
+ create_table :<%= table_prefix %>_verification_keys do
3
+ foreign_key :id, :<%= table_prefix.pluralize %>, primary_key: true, type: :Bignum
4
4
  String :key, null: false
5
5
  DateTime :requested_at, null: false, default: Sequel::CURRENT_TIMESTAMP
6
6
  DateTime :email_last_sent, null: false, default: Sequel::CURRENT_TIMESTAMP
@@ -1,6 +1,6 @@
1
1
  # Used by the verify login change feature
2
- create_table :account_login_change_keys do
3
- foreign_key :id, :accounts, primary_key: true, type: :Bignum
2
+ create_table :<%= table_prefix %>_login_change_keys do
3
+ foreign_key :id, :<%= table_prefix.pluralize %>, primary_key: true, type: :Bignum
4
4
  String :key, null: false
5
5
  String :login, null: false
6
6
  DateTime :deadline, null: false
@@ -1,13 +1,13 @@
1
1
  # Used by the webauthn feature
2
- create_table :account_webauthn_user_ids do
3
- foreign_key :id, :accounts, primary_key: true, type: :Bignum
2
+ create_table :<%= table_prefix %>_webauthn_user_ids do
3
+ foreign_key :id, :<%= table_prefix.pluralize %>, primary_key: true, type: :Bignum
4
4
  String :webauthn_id, null: false
5
5
  end
6
- create_table :account_webauthn_keys do
7
- foreign_key :account_id, :accounts, type: :Bignum
6
+ create_table :<%= table_prefix %>_webauthn_keys do
7
+ foreign_key :<%= table_prefix %>_id, :<%= table_prefix.pluralize %>, type: :Bignum
8
8
  String :webauthn_id
9
9
  String :public_key, null: false
10
10
  Integer :sign_count, null: false
11
11
  Time :last_use, null: false, default: Sequel::CURRENT_TIMESTAMP
12
- primary_key [:account_id, :webauthn_id]
12
+ primary_key [:<%= table_prefix %>_id, :webauthn_id]
13
13
  end
@@ -13,6 +13,9 @@ module Rodauth
13
13
  desc: "Rodauth features to create tables for (otp, sms_codes, single_session, account_expiration etc.)",
14
14
  default: %w[]
15
15
 
16
+ class_option :prefix, optional: true, type: :string,
17
+ desc: "Change prefix for generated tables (default: account)"
18
+
16
19
  class_option :name, optional: true, type: :string,
17
20
  desc: "Name of the generated migration file"
18
21
 
@@ -22,10 +25,24 @@ module Rodauth
22
25
  migration_template "db/migrate/create_rodauth.rb", File.join(db_migrate_path, "#{migration_name}.rb")
23
26
  end
24
27
 
28
+ def show_instructions
29
+ # skip if called from install generator, it already adds configuration
30
+ return if current_command_chain.include?(:generate_rodauth_migration)
31
+ return unless options[:prefix] && behavior == :invoke
32
+
33
+ configuration = CONFIGURATION.values_at(*features.map(&:to_sym))
34
+ .flat_map(&:to_a)
35
+ .map { |config, format| "#{config} :#{format % { plural: table_prefix.pluralize, singular: table_prefix }}" }
36
+ .join("\n")
37
+ .indent(2)
38
+
39
+ say "\nAdd the following to your Rodauth configuration:\n\n#{configuration}"
40
+ end
41
+
25
42
  private
26
43
 
27
44
  def migration_name
28
- options[:name] || "create_rodauth_#{features.join("_")}"
45
+ options[:name] || ["create_rodauth", *options[:prefix], *features].join("_")
29
46
  end
30
47
 
31
48
  def migration_content
@@ -64,6 +81,31 @@ module Rodauth
64
81
  Dir["#{MIGRATION_DIR}/*.erb"].map { |filename| File.basename(filename, ".erb") }
65
82
  end
66
83
 
84
+ def table_prefix
85
+ options[:prefix]&.singularize || "account"
86
+ end
87
+
88
+ CONFIGURATION = {
89
+ base: { accounts_table: "%{plural}" },
90
+ remember: { remember_table: "%{singular}_remember_keys" },
91
+ verify_account: { verify_account_table: "%{singular}_verification_keys" },
92
+ verify_login_change: { verify_login_change_table: "%{singular}_login_change_keys" },
93
+ reset_password: { reset_password_table: "%{singular}_password_reset_keys" },
94
+ email_auth: { email_auth_table: "%{singular}_email_auth_keys" },
95
+ otp: { otp_keys_table: "%{singular}_otp_keys" },
96
+ sms_codes: { sms_codes_table: "%{singular}_sms_codes" },
97
+ recovery_codes: { recovery_codes_table: "%{singular}_recovery_codes" },
98
+ webauthn: { webauthn_keys_table: "%{singular}_webauthn_keys", webauthn_user_ids_table: "%{singular}_webauthn_user_ids", webauthn_keys_account_id_column: "%{singular}_id" },
99
+ lockout: { account_login_failures_table: "%{singular}_login_failures", account_lockouts_table: "%{singular}_lockouts" },
100
+ active_sessions: { active_sessions_table: "%{singular}_active_session_keys", active_sessions_account_id_column: "%{singular}_id" },
101
+ account_expiration: { account_activity_table: "%{singular}_activity_times" },
102
+ password_expiration: { password_expiration_table: "%{singular}_password_change_times" },
103
+ single_session: { single_session_table: "%{singular}_session_keys" },
104
+ audit_logging: { audit_logging_table: "%{singular}_authentication_audit_logs", audit_logging_account_id_column: "%{singular}_id" },
105
+ disallow_password_reuse: { previous_password_hash_table: "%{singular}_previous_password_hashes", previous_password_account_id_column: "%{singular}_id" },
106
+ jwt_refresh: { jwt_refresh_token_table: "%{singular}_jwt_refresh_keys", jwt_refresh_token_account_id_column: "%{singular}_id" },
107
+ }
108
+
67
109
  if defined?(::ActiveRecord::Railtie) # Active Record
68
110
  include ::ActiveRecord::Generators::Migration
69
111
 
@@ -1,16 +1,18 @@
1
1
  class RodauthMailer < ApplicationMailer
2
+ default to: -> { @rodauth.email_to }, from: -> { @rodauth.email_from }
3
+
2
4
  def verify_account(name, account_id, key)
3
5
  @rodauth = rodauth(name, account_id) { @verify_account_key_value = key }
4
6
  @account = @rodauth.rails_account
5
7
 
6
- mail to: @account.email, subject: @rodauth.verify_account_email_subject
8
+ mail subject: @rodauth.verify_account_email_subject
7
9
  end
8
10
 
9
11
  def reset_password(name, account_id, key)
10
12
  @rodauth = rodauth(name, account_id) { @reset_password_key_value = key }
11
13
  @account = @rodauth.rails_account
12
14
 
13
- mail to: @account.email, subject: @rodauth.reset_password_email_subject
15
+ mail subject: @rodauth.reset_password_email_subject
14
16
  end
15
17
 
16
18
  def verify_login_change(name, account_id, key)
@@ -25,28 +27,28 @@ class RodauthMailer < ApplicationMailer
25
27
  @rodauth = rodauth(name, account_id)
26
28
  @account = @rodauth.rails_account
27
29
 
28
- mail to: @account.email, subject: @rodauth.password_changed_email_subject
30
+ mail subject: @rodauth.password_changed_email_subject
29
31
  end
30
32
 
31
33
  # def reset_password_notify(name, account_id)
32
34
  # @rodauth = rodauth(name, account_id)
33
35
  # @account = @rodauth.rails_account
34
36
 
35
- # mail to: @account.email, subject: @rodauth.reset_password_notify_email_subject
37
+ # mail subject: @rodauth.reset_password_notify_email_subject
36
38
  # end
37
39
 
38
40
  # def email_auth(name, account_id, key)
39
41
  # @rodauth = rodauth(name, account_id) { @email_auth_key_value = key }
40
42
  # @account = @rodauth.rails_account
41
43
 
42
- # mail to: @account.email, subject: @rodauth.email_auth_email_subject
44
+ # mail subject: @rodauth.email_auth_email_subject
43
45
  # end
44
46
 
45
47
  # def unlock_account(name, account_id, key)
46
48
  # @rodauth = rodauth(name, account_id) { @unlock_account_key_value = key }
47
49
  # @account = @rodauth.rails_account
48
50
 
49
- # mail to: @account.email, subject: @rodauth.unlock_account_email_subject
51
+ # mail subject: @rodauth.unlock_account_email_subject
50
52
  # end
51
53
 
52
54
  private
@@ -1,18 +1,56 @@
1
+ require "sequel/core"
2
+
1
3
  class RodauthMain < Rodauth::Rails::Auth
2
4
  configure do
3
5
  # List of authentication features that are loaded.
4
6
  enable :create_account, :verify_account, :verify_account_grace_period,
5
7
  :login, :logout<%= ", :remember" unless jwt? %><%= ", :json" if json? %><%= ", :jwt" if jwt? %>,
6
8
  :reset_password, :change_password, :change_password_notify,
7
- :change_login, :verify_login_change, :close_account
9
+ :change_login, :verify_login_change, :close_account<%= ", :argon2" if argon2? %>
8
10
 
9
11
  # See the Rodauth documentation for the list of available config options:
10
12
  # http://rodauth.jeremyevans.net/documentation.html
11
13
 
12
14
  # ==> General
15
+ <% if sequel_activerecord_integration? -%>
16
+ # Initialize Sequel and have it reuse Active Record's database connection.
17
+ <% if RUBY_ENGINE == "jruby" -%>
18
+ db Sequel.connect("jdbc:<%= sequel_adapter %>://", extensions: :activerecord_connection, keep_reference: false)
19
+ <% else -%>
20
+ db Sequel.<%= sequel_adapter %>(extensions: :activerecord_connection, keep_reference: false)
21
+ <% end -%>
22
+
23
+ <% end -%>
24
+ # Change prefix of table and foreign key column names from default "account"
25
+ <% if table -%>
26
+ accounts_table :<%= table_prefix.pluralize %>
27
+ verify_account_table :<%= table_prefix %>_verification_keys
28
+ verify_login_change_table :<%= table_prefix %>_login_change_keys
29
+ reset_password_table :<%= table_prefix %>_password_reset_keys
30
+ <% unless jwt? -%>
31
+ remember_table :<%= table_prefix %>_remember_keys
32
+ <% end -%>
33
+ <% else -%>
34
+ # accounts_table :users
35
+ # verify_account_table :user_verification_keys
36
+ # verify_login_change_table :user_login_change_keys
37
+ # reset_password_table :user_password_reset_keys
38
+ <% unless jwt? -%>
39
+ # remember_table :user_remember_keys
40
+ <% end -%>
41
+ <% end -%>
42
+
13
43
  # The secret key used for hashing public-facing tokens for various features.
14
44
  # Defaults to Rails `secret_key_base`, but you can use your own secret key.
15
45
  # hmac_secret "<%= SecureRandom.hex(64) %>"
46
+ <% if argon2? -%>
47
+
48
+ # Use a rotatable password pepper when hashing passwords with Argon2.
49
+ # argon2_secret "<SECRET_KEY>"
50
+
51
+ # Since we're using argon2, prevent loading the bcrypt gem to save memory.
52
+ require_bcrypt? false
53
+ <% end -%>
16
54
  <% if jwt? -%>
17
55
 
18
56
  # Set JWT secret, which is used to cryptographically protect the token.
@@ -28,10 +66,13 @@ class RodauthMain < Rodauth::Rails::Auth
28
66
  # require_login_confirmation? false
29
67
  <% end -%>
30
68
 
31
- # Specify the controller used for view rendering and CSRF verification.
69
+ # Use path prefix for all routes.
70
+ # prefix "/auth"
71
+
72
+ # Specify the controller used for view rendering, CSRF, and callbacks.
32
73
  rails_controller { RodauthController }
33
74
 
34
- # Set on Rodauth controller with the title of the current page.
75
+ # Set in Rodauth controller instance with the title of the current page.
35
76
  title_instance_variable :@page_title
36
77
 
37
78
  # Store account status in an integer column without foreign key constraint.
@@ -40,14 +81,13 @@ class RodauthMain < Rodauth::Rails::Auth
40
81
  # Store password hash in a column instead of a separate table.
41
82
  account_password_hash_column :password_hash
42
83
 
43
- # Passwords shorter than 8 characters are considered weak according to OWASP.
44
- password_minimum_length 8
45
- # bcrypt has a maximum input length of 72 bytes, truncating any extra bytes.
46
- password_maximum_bytes 72
47
-
48
84
  # Set password when creating account instead of when verifying.
49
85
  verify_account_set_password? false
50
86
 
87
+ # Change some default param keys.
88
+ # login_param "email"
89
+ # password_confirm_param "confirm_password"
90
+
51
91
  # Redirect back to originally requested location after authentication.
52
92
  # login_return_to_requested_location? true
53
93
  # two_factor_auth_return_to_requested_location? true # if using MFA
@@ -110,8 +150,27 @@ class RodauthMain < Rodauth::Rails::Auth
110
150
  # password_too_short_message { "needs to have at least #{password_minimum_length} characters" }
111
151
  # login_does_not_meet_requirements_message { "invalid email#{", #{login_requirement_message}" if login_requirement_message}" }
112
152
 
113
- # Change minimum number of password characters required when creating an account.
114
- # password_minimum_length 8
153
+ # Passwords shorter than 8 characters are considered weak according to OWASP.
154
+ password_minimum_length 8
155
+ <% if argon2? -%>
156
+ # Having a maximum password length set prevents long password DoS attacks.
157
+ password_maximum_length 64
158
+ <% else -%>
159
+ # bcrypt has a maximum input length of 72 bytes, truncating any extra bytes.
160
+ password_maximum_bytes 72
161
+ <% end -%>
162
+
163
+ # Custom password complexity requirements (alternative to password_complexity feature).
164
+ # password_meets_requirements? do |password|
165
+ # super(password) && password_complex_enough?(password)
166
+ # end
167
+ # auth_class_eval do
168
+ # def password_complex_enough?(password)
169
+ # return true if password.match?(/\d/) && password.match?(/[^a-zA-Z\d]/)
170
+ # set_password_requirement_error_message(:password_simple, "requires one number and one special character")
171
+ # false
172
+ # end
173
+ # end
115
174
  <% unless jwt? -%>
116
175
 
117
176
  # ==> Remember Feature
@@ -1,5 +1,5 @@
1
1
  <% if defined?(ActiveRecord::Railtie) -%>
2
- class Account < ApplicationRecord
2
+ class <%= table_prefix.camelize %> < ApplicationRecord
3
3
  include Rodauth::Rails.model
4
4
  <% if ActiveRecord.version >= Gem::Version.new("7.0") -%>
5
5
  enum :status, unverified: 1, verified: 2, closed: 3
@@ -8,7 +8,7 @@ class Account < ApplicationRecord
8
8
  <% end -%>
9
9
  end
10
10
  <% else -%>
11
- class Account < Sequel::Model
11
+ class <%= table_prefix.camelize %> < Sequel::Model
12
12
  include Rodauth::Rails.model
13
13
  plugin :enum
14
14
  enum :status, unverified: 1, verified: 2, closed: 3
@@ -0,0 +1,13 @@
1
+ <% cred = rodauth.webauthn_credential_options_for_get %>
2
+
3
+ <%= form_with url: rodauth.webauthn_auth_form_path, method: :post, id: "webauthn-auth-form", data: { credential_options: cred.as_json.to_json, turbo: false }, class: "w-full max-w-sm" do |form| %>
4
+ <%= form.hidden_field rodauth.login_param, value: params[rodauth.login_param] %>
5
+ <%= form.hidden_field rodauth.webauthn_auth_challenge_param, value: cred.challenge %>
6
+ <%= form.hidden_field rodauth.webauthn_auth_challenge_hmac_param, value: rodauth.compute_hmac(cred.challenge) %>
7
+ <%= form.text_field rodauth.webauthn_auth_param, value: "", id: "webauthn-auth", class: "hidden", aria: { hidden: "true" } %>
8
+ <div id="webauthn-auth-button">
9
+ <%= form.submit rodauth.webauthn_auth_button, class: "w-full px-8 py-3 cursor-pointer font-semibold text-sm rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-600 dark:bg-emerald-400 dark:hover:bg-emerald-500 dark:text-gray-900 dark:focus:ring-emerald-400 dark:focus:ring-offset-current" %>
10
+ </div>
11
+ <% end %>
12
+
13
+ <%= javascript_include_tag rodauth.webauthn_auth_js_path, extname: false %>
@@ -0,0 +1,21 @@
1
+ <%= form_with url: rodauth.webauthn_remove_path, method: :post, id: "webauthn-remove-form", data: { turbo: false }, class: "w-full max-w-sm" do |form| %>
2
+ <% if rodauth.two_factor_modifications_require_password? %>
3
+ <div class="mb-6">
4
+ <%= form.label "password", rodauth.password_label, class: "block text-sm font-semibold" %>
5
+ <%= form.password_field rodauth.password_param, value: "", id: "password", autocomplete: rodauth.password_field_autocomplete_value, required: true, class: "mt-2 text-sm w-full px-3 py-2 border rounded-md dark:bg-gray-900 dark:text-gray-100 dark:focus:bg-gray-800 #{rodauth.field_error(rodauth.password_param) ? "border-red-600 focus:ring-red-600 focus:border-red-600 dark:border-red-400 dark:focus:ring-red-400" : "border-gray-300 dark:border-gray-700 dark:focus:border-emerald-400 dark:focus:ring-emerald-400" }", aria: ({ invalid: true, describedby: "password_error_message" } if rodauth.field_error(rodauth.password_param)) %>
6
+ <%= content_tag(:span, rodauth.field_error(rodauth.password_param), class: "block mt-1 text-red-600 text-xs dark:text-red-400", id: "password_error_message") if rodauth.field_error(rodauth.password_param) %>
7
+ </div>
8
+ <% end %>
9
+
10
+ <fieldset class="mb-6">
11
+ <% rodauth.account_webauthn_usage.each do |id, last_use| %>
12
+ <div class="flex items-center space-x-2">
13
+ <%= form.radio_button rodauth.webauthn_remove_param, id, id: "webauthn-remove-#{id}", class: "dark:bg-gray-900 dark:border-gray-600 dark:checked:bg-current dark:checked:border-current dark:checked:text-emerald-400 dark:focus:ring-emerald-400 dark:focus:ring-offset-gray-900" %>
14
+ <%= form.label "webauthn-remove-#{id}", "Last use: #{last_use}", class: "text-sm" %>
15
+ </div>
16
+ <% end %>
17
+ <%= content_tag(:span, rodauth.field_error(rodauth.webauthn_remove_param), class: "block mt-1 text-red-600 text-xs dark:text-red-400", id: "webauthn_remove_error_message") if rodauth.field_error(rodauth.webauthn_remove_param) %>
18
+ </fieldset>
19
+
20
+ <%= form.submit rodauth.webauthn_remove_button, class: "w-full px-8 py-3 cursor-pointer font-semibold text-sm rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-600 dark:bg-emerald-400 dark:hover:bg-emerald-500 dark:text-gray-900 dark:focus:ring-emerald-400 dark:focus:ring-offset-current" %>
21
+ <% end %>
@@ -0,0 +1,21 @@
1
+ <% cred = rodauth.new_webauthn_credential %>
2
+
3
+ <%= form_with url: request.path, method: :post, id: "webauthn-setup-form", data: { credential_options: cred.as_json.to_json, turbo: false }, class: "w-full max-w-sm" do |form| %>
4
+ <%= form.hidden_field rodauth.webauthn_setup_challenge_param, value: cred.challenge %>
5
+ <%= form.hidden_field rodauth.webauthn_setup_challenge_hmac_param, value: rodauth.compute_hmac(cred.challenge) %>
6
+ <%= form.text_field rodauth.webauthn_setup_param, value: "", id: "webauthn-setup", class: "hidden", aria: { hidden: "true" } %>
7
+
8
+ <% if rodauth.two_factor_modifications_require_password? %>
9
+ <div class="mb-6">
10
+ <%= form.label "password", rodauth.password_label, class: "block text-sm font-semibold" %>
11
+ <%= form.password_field rodauth.password_param, value: "", id: "password", autocomplete: rodauth.password_field_autocomplete_value, required: true, class: "mt-2 text-sm w-full px-3 py-2 border rounded-md dark:bg-gray-900 dark:text-gray-100 dark:focus:bg-gray-800 #{rodauth.field_error(rodauth.password_param) ? "border-red-600 focus:ring-red-600 focus:border-red-600 dark:border-red-400 dark:focus:ring-red-400" : "border-gray-300 dark:border-gray-700 dark:focus:border-emerald-400 dark:focus:ring-emerald-400" }", aria: ({ invalid: true, describedby: "password_error_message" } if rodauth.field_error(rodauth.password_param)) %>
12
+ <%= content_tag(:span, rodauth.field_error(rodauth.password_param), class: "block mt-1 text-red-600 text-xs dark:text-red-400", id: "password_error_message") if rodauth.field_error(rodauth.password_param) %>
13
+ </div>
14
+ <% end %>
15
+
16
+ <div id="webauthn-setup-button">
17
+ <%= form.submit rodauth.webauthn_setup_button, class: "w-full px-8 py-3 cursor-pointer font-semibold text-sm rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-600 dark:bg-emerald-400 dark:hover:bg-emerald-500 dark:text-gray-900 dark:focus:ring-emerald-400 dark:focus:ring-offset-current" %>
18
+ </div>
19
+ <% end %>
20
+
21
+ <%= javascript_include_tag rodauth.webauthn_setup_js_path, extname: false %>
@@ -1,10 +1,10 @@
1
- <% cred = rodauth.webauth_credential_options_for_get %>
1
+ <% cred = rodauth.webauthn_credential_options_for_get %>
2
2
 
3
3
  <%= form_with url: rodauth.webauthn_auth_form_path, method: :post, id: "webauthn-auth-form", data: { credential_options: cred.as_json.to_json, turbo: false } do |form| %>
4
4
  <%= form.hidden_field rodauth.login_param, value: params[rodauth.login_param] %>
5
5
  <%= form.hidden_field rodauth.webauthn_auth_challenge_param, value: cred.challenge %>
6
6
  <%= form.hidden_field rodauth.webauthn_auth_challenge_hmac_param, value: rodauth.compute_hmac(cred.challenge) %>
7
- <%= form.text_field rodauth.webauthn_auth_param, value: "", id: "webauthn-auth", aria: { hidden: "true" } %>
7
+ <%= form.text_field rodauth.webauthn_auth_param, value: "", id: "webauthn-auth", class: "d-none", aria: { hidden: "true" } %>
8
8
  <div id="webauthn-auth-button">
9
9
  <div class="form-group mb-3">
10
10
  <%= form.submit rodauth.webauthn_auth_button, class: "btn btn-primary" %>
@@ -12,4 +12,4 @@
12
12
  </div>
13
13
  <% end %>
14
14
 
15
- <%= javascript_include_tag rodauth.webauthn_auth_js_path %>
15
+ <%= javascript_include_tag rodauth.webauthn_auth_js_path, extname: false %>
@@ -1,9 +1,9 @@
1
1
  <% cred = rodauth.new_webauthn_credential %>
2
2
 
3
- <%= form_with url: rodauth.webauthn_setup_path, method: :post, id: "webauthn-setup-form", data: { credential_options: cred.as_json.to_json, turbo: false } do |form| %>
3
+ <%= form_with url: request.path, method: :post, id: "webauthn-setup-form", data: { credential_options: cred.as_json.to_json, turbo: false } do |form| %>
4
4
  <%= form.hidden_field rodauth.webauthn_setup_challenge_param, value: cred.challenge %>
5
5
  <%= form.hidden_field rodauth.webauthn_setup_challenge_hmac_param, value: rodauth.compute_hmac(cred.challenge) %>
6
- <%= form.text_field rodauth.webauthn_setup_param, value: "", id: "webauthn-setup", aria: { hidden: "true" } %>
6
+ <%= form.text_field rodauth.webauthn_setup_param, value: "", id: "webauthn-setup", class: "d-none", aria: { hidden: "true" } %>
7
7
 
8
8
  <% if rodauth.two_factor_modifications_require_password? %>
9
9
  <div class="form-group mb-3">
@@ -13,11 +13,11 @@
13
13
  </div>
14
14
  <% end %>
15
15
 
16
- <div id="webauthn-setup-button">
16
+ <div id="webauthn-setup-button">
17
17
  <div class="form-group mb-3">
18
18
  <%= form.submit rodauth.webauthn_setup_button, class: "btn btn-primary" %>
19
19
  </div>
20
20
  </div>
21
21
  <% end %>
22
22
 
23
- <%= javascript_include_tag rodauth.webauthn_setup_js_path %>
23
+ <%= javascript_include_tag rodauth.webauthn_setup_js_path, extname: false %>
@@ -1,10 +1,10 @@
1
1
  # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
2
2
  one:
3
3
  email: freddie@queen.com
4
- password_hash: <%%= BCrypt::Password.create("password", cost: BCrypt::Engine::MIN_COST) %>
4
+ password_hash: <%%= RodauthMain.allocate.password_hash("password") %>
5
5
  status: verified
6
6
 
7
7
  two:
8
8
  email: brian@queen.com
9
- password_hash: <%%= BCrypt::Password.create("password", cost: BCrypt::Engine::MIN_COST) %>
9
+ password_hash: <%%= RodauthMain.allocate.password_hash("password") %>
10
10
  status: verified
@@ -82,6 +82,25 @@ module Rodauth
82
82
  super
83
83
  end
84
84
  end
85
+
86
+ # The Rack input might not be rewindable, so ensure we parse the JSON
87
+ # request body in Rails, and avoid parsing it again in Roda.
88
+ def POST
89
+ if content_type =~ /json/
90
+ env["roda.json_params"] = scope.rails_request.POST.to_hash
91
+ end
92
+ super
93
+ end
94
+
95
+ unless ActionPack.version < Gem::Version.new("5.0")
96
+ # When calling a Rodauth method that redirects inside the Rails
97
+ # router, Roda's after hook that commits the flash would never get
98
+ # called, so we make sure to commit the flash beforehand.
99
+ def redirect(*)
100
+ scope.rails_request.commit_flash
101
+ super
102
+ end
103
+ end
85
104
  end
86
105
  end
87
106
  end
@@ -60,16 +60,6 @@ module Rodauth
60
60
 
61
61
  private
62
62
 
63
- unless ActionPack.version < Gem::Version.new("5.0")
64
- # When calling a Rodauth method that redirects inside the Rails
65
- # router, Roda's after hook that commits the flash would never get
66
- # called, so we make sure to commit the flash beforehand.
67
- def redirect(*)
68
- rails_request.commit_flash
69
- super
70
- end
71
- end
72
-
73
63
  def instantiate_rails_account
74
64
  if defined?(ActiveRecord::Base) && rails_account_model < ActiveRecord::Base
75
65
  rails_account_model.instantiate(account.stringify_keys)
@@ -22,8 +22,8 @@ module Rodauth
22
22
 
23
23
  # ActionMailer subclass for correct email delivering.
24
24
  class Mailer < ActionMailer::Base
25
- def create_email(**options)
26
- mail(**options)
25
+ def create_email(options)
26
+ mail(options)
27
27
  end
28
28
  end
29
29
  end
@@ -41,7 +41,7 @@ module Rodauth
41
41
  # Checks whether we're in an internal request and host was not set,
42
42
  # or the request doesn't exist such as with path_class_methods feature.
43
43
  def missing_host?
44
- internal_request? && request.host == INVALID_DOMAIN || scope.nil?
44
+ internal_request? && (request.host.nil? || request.host == INVALID_DOMAIN) || scope.nil?
45
45
  end
46
46
 
47
47
  def rails_url_options
@@ -1,5 +1,5 @@
1
1
  module Rodauth
2
2
  module Rails
3
- VERSION = "1.7.0"
3
+ VERSION = "1.8.0"
4
4
  end
5
5
  end
@@ -17,7 +17,7 @@ Gem::Specification.new do |spec|
17
17
  spec.require_paths = ["lib"]
18
18
 
19
19
  spec.add_dependency "railties", ">= 4.2", "< 8"
20
- spec.add_dependency "rodauth", "~> 2.25"
20
+ spec.add_dependency "rodauth", "~> 2.28"
21
21
  spec.add_dependency "roda", "~> 3.55"
22
22
  spec.add_dependency "sequel-activerecord_connection", "~> 1.1"
23
23
  spec.add_dependency "rodauth-model", "~> 0.2"
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: 1.7.0
4
+ version: 1.8.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: 2022-12-21 00:00:00.000000000 Z
11
+ date: 2023-02-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: railties
@@ -36,14 +36,14 @@ dependencies:
36
36
  requirements:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: '2.25'
39
+ version: '2.28'
40
40
  type: :runtime
41
41
  prerelease: false
42
42
  version_requirements: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: '2.25'
46
+ version: '2.28'
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: roda
49
49
  requirement: !ruby/object:Gem::Requirement
@@ -219,11 +219,11 @@ files:
219
219
  - lib/generators/rodauth/migration/sequel/webauthn.erb
220
220
  - lib/generators/rodauth/migration_generator.rb
221
221
  - lib/generators/rodauth/templates/INSTRUCTIONS
222
- - lib/generators/rodauth/templates/app/controllers/rodauth_controller.rb
223
- - lib/generators/rodauth/templates/app/mailers/rodauth_mailer.rb
224
- - lib/generators/rodauth/templates/app/misc/rodauth_app.rb
225
- - lib/generators/rodauth/templates/app/misc/rodauth_main.rb
226
- - lib/generators/rodauth/templates/app/models/account.rb
222
+ - lib/generators/rodauth/templates/app/controllers/rodauth_controller.rb.tt
223
+ - lib/generators/rodauth/templates/app/mailers/rodauth_mailer.rb.tt
224
+ - lib/generators/rodauth/templates/app/misc/rodauth_app.rb.tt
225
+ - lib/generators/rodauth/templates/app/misc/rodauth_main.rb.tt
226
+ - lib/generators/rodauth/templates/app/models/account.rb.tt
227
227
  - lib/generators/rodauth/templates/app/views/rodauth/_email_auth_request_form.html.erb
228
228
  - lib/generators/rodauth/templates/app/views/rodauth/_login_form.html.erb
229
229
  - lib/generators/rodauth/templates/app/views/rodauth/_login_form_footer.html.erb
@@ -286,6 +286,9 @@ files:
286
286
  - lib/generators/rodauth/templates/app/views/rodauth/tailwind/verify_account.html.erb
287
287
  - lib/generators/rodauth/templates/app/views/rodauth/tailwind/verify_account_resend.html.erb
288
288
  - lib/generators/rodauth/templates/app/views/rodauth/tailwind/verify_login_change.html.erb
289
+ - lib/generators/rodauth/templates/app/views/rodauth/tailwind/webauthn_auth.html.erb
290
+ - lib/generators/rodauth/templates/app/views/rodauth/tailwind/webauthn_remove.html.erb
291
+ - lib/generators/rodauth/templates/app/views/rodauth/tailwind/webauthn_setup.html.erb
289
292
  - lib/generators/rodauth/templates/app/views/rodauth/two_factor_auth.html.erb
290
293
  - lib/generators/rodauth/templates/app/views/rodauth/two_factor_disable.html.erb
291
294
  - lib/generators/rodauth/templates/app/views/rodauth/two_factor_manage.html.erb
@@ -304,10 +307,9 @@ files:
304
307
  - lib/generators/rodauth/templates/app/views/rodauth_mailer/unlock_account.text.erb
305
308
  - lib/generators/rodauth/templates/app/views/rodauth_mailer/verify_account.text.erb
306
309
  - lib/generators/rodauth/templates/app/views/rodauth_mailer/verify_login_change.text.erb
307
- - lib/generators/rodauth/templates/config/initializers/rodauth.rb
308
- - lib/generators/rodauth/templates/config/initializers/sequel.rb
309
- - lib/generators/rodauth/templates/db/migrate/create_rodauth.rb
310
- - lib/generators/rodauth/templates/test/fixtures/accounts.yml
310
+ - lib/generators/rodauth/templates/config/initializers/rodauth.rb.tt
311
+ - lib/generators/rodauth/templates/db/migrate/create_rodauth.rb.tt
312
+ - lib/generators/rodauth/templates/test/fixtures/accounts.yml.tt
311
313
  - lib/generators/rodauth/views_generator.rb
312
314
  - lib/rodauth-rails.rb
313
315
  - lib/rodauth/rails.rb
@@ -349,7 +351,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
349
351
  - !ruby/object:Gem::Version
350
352
  version: '0'
351
353
  requirements: []
352
- rubygems_version: 3.3.3
354
+ rubygems_version: 3.4.6
353
355
  signing_key:
354
356
  specification_version: 4
355
357
  summary: Provides Rails integration for Rodauth.
@@ -1,4 +0,0 @@
1
- require "sequel/core"
2
-
3
- # initialize Sequel and have it reuse Active Record's database connection
4
- DB = Sequel.connect("<%= sequel_uri_scheme %>://", extensions: :activerecord_connection)